#!/usr/bin/env python3
"""
Helix-CDC CLI

Commands:
  validate                 Validate a receipt
  regen-procedural         Procedural GGUF generation (valid container, noise weights)
  fidelity-create-residual Create XOR residual for fidelity reconstruction
  fidelity-reconstruct     Reconstruct from baseline + residual (bit-exact)
  fidelity-from-manifest   Reconstruct using manifest instance_residual

See docs/REGEN_TAXONOMY.md for the distinction between procedural and fidelity regen.
"""

import argparse
import json
import sys
from pathlib import Path


def cmd_validate(args):
    """Validate a receipt."""
    from .validate import validate_receipt
    return validate_receipt(args.receipt)


def cmd_regen_procedural(args):
    """Procedural GGUF generation - valid container, NOISE weights (not real model)."""
    from .regrow import regenerate_full_gguf, RegenConfig
    from .regrow.full_regen_stream import get_default_atlas_path

    atlas_path = Path(args.atlas) if args.atlas else get_default_atlas_path()

    config = RegenConfig(
        dna=args.dna,
        entropy=args.entropy,
        atlas_path=atlas_path,
        header_source=Path(args.header_source),
        output_path=Path(args.output),
        slice_mb=args.slice_mb,
        workers=args.workers,
        label=args.label,
        allow_procedural=args.allow_procedural,
    )

    receipt = regenerate_full_gguf(config)
    return 0


def cmd_fidelity_create_residual(args):
    """Create XOR residual for fidelity reconstruction."""
    from .capsules import (
        compute_xor_residual,
        attach_instance_residual,
    )
    from .capsules.instance_residual import compute_file_hash

    baseline_path = Path(args.baseline_gguf)
    regen_path = Path(args.regen_gguf)
    manifest_path = Path(args.manifest)
    residual_relpath = args.residual_out

    # Compute output path relative to manifest directory
    manifest_dir = manifest_path.parent
    residual_abs_path = manifest_dir / residual_relpath

    print(f"=== Clone Mode v0: Attach Residual ===")
    print(f"Baseline: {baseline_path}")
    print(f"Regen:    {regen_path}")
    print(f"Manifest: {manifest_path}")
    print(f"Residual: {residual_abs_path}")
    print()

    # Compute hashes
    print("Computing file hashes...")
    baseline_hash = compute_file_hash(baseline_path)
    regen_hash = compute_file_hash(regen_path)
    print(f"  Baseline SHA256: {baseline_hash[:16]}...")
    print(f"  Regen SHA256:    {regen_hash[:16]}...")

    # Compute XOR residual
    print("Computing XOR residual...")
    residual_sha256 = compute_xor_residual(baseline_path, regen_path, residual_abs_path)
    residual_size = residual_abs_path.stat().st_size
    print(f"  Residual SHA256: {residual_sha256[:16]}...")
    print(f"  Residual size:   {residual_size:,} bytes")

    # Load existing manifest
    if manifest_path.exists():
        manifest_dict = json.loads(manifest_path.read_text())
        print(f"  Loaded existing manifest")
    else:
        manifest_dict = {"format": "helix_hxz", "version": "v0.2.0"}
        print(f"  Created new manifest")

    # Attach instance_residual
    attach_instance_residual(
        manifest_dict,
        mode="clone",
        residual_sha256=residual_sha256,
        residual_relpath=residual_relpath,
        baseline_hash=baseline_hash,
        regen_hash=regen_hash,
        residual_size_bytes=residual_size,
        codec="xor_v0",
    )

    # Write updated manifest
    manifest_path.write_text(json.dumps(manifest_dict, indent=2))
    print()
    print(f"=== Updated manifest: {manifest_path} ===")
    print(json.dumps(manifest_dict.get("instance_residual", {}), indent=2))

    return 0


def cmd_fidelity_reconstruct(args):
    """Fidelity reconstruction from baseline + residual (bit-exact)."""
    from .capsules import reconstruct_regen_from_baseline_and_residual
    from .capsules.instance_residual import compute_file_hash

    baseline_path = Path(args.baseline_gguf)
    residual_path = Path(args.residual)
    output_path = Path(args.output)

    print(f"=== Clone Mode v0: Reconstruct ===")
    print(f"Baseline: {baseline_path}")
    print(f"Residual: {residual_path}")
    print(f"Output:   {output_path}")
    print()

    # Reconstruct
    print("Reconstructing regen from baseline ^ residual...")
    regen_hash = reconstruct_regen_from_baseline_and_residual(
        baseline_path, residual_path, output_path
    )

    print(f"  Output SHA256: {regen_hash[:16]}...")
    print(f"  Output size:   {output_path.stat().st_size:,} bytes")
    print()
    print(f"=== Reconstructed: {output_path} ===")

    return 0


# ============================================
# WO#50: Crypto-DNA CLI Commands
# ============================================

def cmd_dna_inspect(args):
    """Inspect Crypto-DNA structure from a Base64 seed."""
    import json
    from .capsules import (
        b64_to_bigint,
        full_genome_pipeline,
    )

    seed_b64 = args.seed
    print(f"=== Crypto-DNA Inspection ===")
    print(f"Seed (Base64): {seed_b64}")
    print()

    # Basic conversion
    n = b64_to_bigint(seed_b64)
    print(f"BigInt (N): {n}")
    print(f"Bit length: {n.bit_length()} bits")
    print()

    # Full pipeline
    genome = full_genome_pipeline(seed_b64)
    print(f"Factorization:")
    print(f"  p = {genome.p}")
    print(f"  q = {genome.q}")
    print(f"  bifurcation_point = {genome.bifurcation_point}")
    print()

    print(f"P-Stream ({len(genome.p_stream.codons)} codons):")
    print(f"  anchor_offset: {genome.p_stream.anchor_offset}")
    print(f"  growth_params: {genome.p_stream.growth_params}")
    print()

    print(f"Q-Stream ({len(genome.q_stream.codons)} codons):")
    print(f"  anchor_offset: {genome.q_stream.anchor_offset}")
    print(f"  growth_params: {genome.q_stream.growth_params}")

    if args.json:
        result = {
            "seed_b64": seed_b64,
            "bigint": str(n),
            "bit_length": n.bit_length(),
            "p": genome.p,
            "q": genome.q,
            "p_stream_codons": len(genome.p_stream.codons),
            "q_stream_codons": len(genome.q_stream.codons),
        }
        print()
        print(json.dumps(result, indent=2))

    return 0


def cmd_dna_grow(args):
    """Grow phenotype from Crypto-DNA seed."""
    import json
    import hashlib
    from .capsules import full_crypto_regen_pipeline

    seed_b64 = args.seed
    output_path = Path(args.output)

    # Parse shape
    shape = tuple(int(x) for x in args.shape.split(','))

    print(f"=== Crypto-DNA Growth ===")
    print(f"Seed: {seed_b64}")
    print(f"Output shape: {shape}")
    print(f"Output dtype: {args.dtype}")
    print()

    # Run pipeline
    result = full_crypto_regen_pipeline(seed_b64, shape, args.dtype)

    # Save output
    if args.format == 'npy':
        import numpy as np
        np.save(output_path, result.data)
    elif args.format == 'raw':
        if isinstance(result.data, bytes):
            output_path.write_bytes(result.data)
        else:
            output_path.write_bytes(result.data.tobytes())
    else:
        # JSON (for small outputs)
        import numpy as np
        output_path.write_text(json.dumps({
            "shape": list(result.shape),
            "dtype": result.dtype,
            "sha256": result.sha256,
            "growth_steps": result.growth_steps,
            "data_sample": result.data.flatten()[:100].tolist() if hasattr(result.data, 'flatten') else None,
        }, indent=2))

    print(f"Output written to: {output_path}")
    print(f"SHA256: {result.sha256}")
    print(f"Growth steps: {result.growth_steps}")

    return 0


def cmd_dna_bind(args):
    """Bind Crypto-DNA to a capsule manifest."""
    import json
    from .capsules import (
        full_genome_pipeline,
        crt_growth_pipeline,
        CryptoDNA,
    )

    seed_b64 = args.seed
    manifest_path = Path(args.manifest)

    print(f"=== Crypto-DNA Binding ===")
    print(f"Seed: {seed_b64}")
    print(f"Manifest: {manifest_path}")
    print()

    # Run genome pipeline
    genome = full_genome_pipeline(seed_b64)
    crt_result = crt_growth_pipeline(genome.p, genome.q)

    # Build CryptoDNA
    crypto_dna = CryptoDNA(
        dna_b64=seed_b64,
        factorization_p=genome.p,
        factorization_q=genome.q,
        fibpi3d_offsets={
            'x': genome.p_stream.anchor_offset[0],
            'y': genome.p_stream.anchor_offset[1],
            'z': genome.p_stream.anchor_offset[2],
        },
        crt_layout={
            'product': crt_result.product,
            'phenotype_int': crt_result.phenotype_int,
        },
    )

    # Load or create manifest
    if manifest_path.exists():
        manifest_dict = json.loads(manifest_path.read_text())
        print(f"Loaded existing manifest")
    else:
        manifest_dict = {"format": "helix_hxz", "version": "v0.2.0"}
        print(f"Created new manifest")

    # Attach crypto_dna
    manifest_dict["crypto_dna"] = crypto_dna.to_dict()

    # Write updated manifest
    manifest_path.write_text(json.dumps(manifest_dict, indent=2))
    print()
    print(f"=== Updated manifest: {manifest_path} ===")
    print(json.dumps(manifest_dict.get("crypto_dna", {}), indent=2))

    return 0


def cmd_dna_to_digest(args):
    """Convert Crypto-DNA seed to digest (hash summary)."""
    import hashlib
    from .capsules import b64_to_bigint, apply_crypto_padding

    seed_b64 = args.seed

    # Convert and pad
    n = b64_to_bigint(seed_b64)
    n_padded = apply_crypto_padding(n)

    # Compute digest
    padded_bytes = n_padded.to_bytes((n_padded.bit_length() + 7) // 8 or 1, 'big')
    digest = hashlib.sha256(padded_bytes).hexdigest()

    print(f"Seed (Base64): {seed_b64}")
    print(f"Padded BigInt: {n_padded}")
    print(f"Digest (SHA256): {digest}")

    return 0


def cmd_digest_to_dna(args):
    """Look up DNA seed from digest (if stored in manifest)."""
    import json

    digest = args.digest
    manifest_path = Path(args.manifest)

    if not manifest_path.exists():
        print(f"ERROR: Manifest not found: {manifest_path}")
        return 1

    manifest_dict = json.loads(manifest_path.read_text())

    # Check if crypto_dna matches
    crypto_dna = manifest_dict.get("crypto_dna", {})
    if not crypto_dna:
        print(f"ERROR: No crypto_dna in manifest")
        return 1

    dna_b64 = crypto_dna.get("dna_b64", "")

    # Verify by recomputing digest
    from .capsules import b64_to_bigint, apply_crypto_padding
    import hashlib

    n = b64_to_bigint(dna_b64)
    n_padded = apply_crypto_padding(n)
    padded_bytes = n_padded.to_bytes((n_padded.bit_length() + 7) // 8 or 1, 'big')
    computed_digest = hashlib.sha256(padded_bytes).hexdigest()

    if computed_digest == digest:
        print(f"Digest: {digest}")
        print(f"DNA (Base64): {dna_b64}")
        return 0
    else:
        print(f"ERROR: Digest mismatch")
        print(f"  Expected: {digest}")
        print(f"  Computed: {computed_digest}")
        return 1


def cmd_regen_gguf_template(args):
    """Full GGUF regeneration using template library for EXACT reconstruction."""
    from .regrow_templates import regrow_full_model, load_template_library

    template_lib = None
    if args.template_lib:
        template_lib = load_template_library(Path(args.template_lib))

    receipt = regrow_full_model(
        dna_hex=args.dna,
        entropy_seed=args.entropy,
        header_source=Path(args.header_source),
        output_path=Path(args.output),
        template_lib=template_lib,
    )

    # Save receipt
    receipt_path = Path(args.output).with_suffix('.template_receipt.json')
    receipt_path.write_text(json.dumps(receipt, indent=2))
    print(f"Receipt: {receipt_path}")

    return 0 if receipt.get('full_model', {}).get('exact_match') else 1


def cmd_fidelity_from_manifest(args):
    """Fidelity reconstruction using baseline + manifest instance_residual (bit-exact)."""
    import hashlib
    from .capsules import reconstruct_regen_from_baseline_and_residual
    from .capsules.instance_residual import compute_file_hash

    baseline_path = Path(args.baseline_gguf)
    manifest_path = Path(args.manifest)
    output_path = Path(args.output)

    print(f"=== Clone Mode v0: Regen from Manifest ===")
    print(f"Baseline: {baseline_path}")
    print(f"Manifest: {manifest_path}")
    print(f"Output:   {output_path}")
    print()

    # Load manifest
    if not manifest_path.exists():
        print(f"ERROR: Manifest not found: {manifest_path}")
        return 1
    manifest_dict = json.loads(manifest_path.read_text())

    # Extract instance_residual block
    ir = manifest_dict.get("instance_residual") or {}
    residual_rel = ir.get("residual_bytes_relpath")
    stored_residual_sha = ir.get("residual_sha256")
    expected_baseline_sha = ir.get("baseline_hash")
    expected_regen_sha = ir.get("regen_hash")

    if not residual_rel:
        print("ERROR: No instance_residual.residual_bytes_relpath found in manifest")
        return 1

    # Resolve residual absolute path
    residual_path = manifest_path.parent / residual_rel
    if not residual_path.exists():
        print(f"ERROR: Residual file not found: {residual_path}")
        return 1

    print(f"Residual: {residual_path}")
    print()

    # Verify baseline hash if present
    if expected_baseline_sha:
        print("Verifying baseline hash...")
        actual_baseline_sha = compute_file_hash(baseline_path)
        if actual_baseline_sha != expected_baseline_sha:
            print(f"ERROR: Baseline SHA mismatch!")
            print(f"  Manifest: {expected_baseline_sha}")
            print(f"  Actual:   {actual_baseline_sha}")
            return 1
        print(f"  Baseline hash verified: {actual_baseline_sha[:16]}...")

    # Verify residual hash if stored
    if stored_residual_sha:
        print("Verifying residual hash...")
        data = residual_path.read_bytes()
        actual_residual_sha = hashlib.sha256(data).hexdigest()
        if actual_residual_sha != stored_residual_sha:
            print(f"ERROR: Residual SHA mismatch!")
            print(f"  Manifest: {stored_residual_sha}")
            print(f"  Actual:   {actual_residual_sha}")
            return 1
        print(f"  Residual hash verified: {actual_residual_sha[:16]}...")

    # Reconstruct
    print()
    print("Reconstructing regen from baseline ^ residual...")
    regen_hash = reconstruct_regen_from_baseline_and_residual(
        baseline_path, residual_path, output_path
    )

    # Verify output matches expected regen hash
    if expected_regen_sha and regen_hash != expected_regen_sha:
        print(f"WARNING: Output hash differs from manifest regen_hash")
        print(f"  Expected: {expected_regen_sha}")
        print(f"  Got:      {regen_hash}")

    print(f"  Output SHA256: {regen_hash}")
    print(f"  Output size:   {output_path.stat().st_size:,} bytes")
    print()
    print(f"=== Reconstructed: {output_path} ===")

    return 0


# ============================================
# Echo Self-Awareness Commands
# ============================================

def cmd_receipts_index(args):
    """Scan receipts/ directory and emit index of all receipts."""
    import os
    from datetime import datetime

    receipts_dir = Path(args.receipts_dir).expanduser().resolve()

    if not receipts_dir.exists():
        print(f"ERROR: Receipts directory not found: {receipts_dir}")
        return 1

    index = []
    errors = []

    # Walk all JSON files in receipts directory
    for root, dirs, files in os.walk(receipts_dir):
        for f in files:
            if not f.endswith('.json'):
                continue

            file_path = Path(root) / f
            rel_path = file_path.relative_to(receipts_dir)

            try:
                data = json.loads(file_path.read_text())

                # Extract common fields
                entry = {
                    "path": str(rel_path),
                    "size_bytes": file_path.stat().st_size,
                }

                # Try to get timestamp
                for ts_key in ['timestamp_utc', 'timestamp', 'created', 'date']:
                    if ts_key in data:
                        entry["timestamp"] = data[ts_key]
                        break

                # Try to get claim/purpose/summary
                for claim_key in ['claim', 'purpose', 'summary', 'conclusion', 'description', 'overall_verdict']:
                    if claim_key in data:
                        claim = str(data[claim_key])
                        entry["claim"] = claim[:120] + "..." if len(claim) > 120 else claim
                        break

                # Try to get status/verdict
                for status_key in ['status', 'verdict', 'overall_verdict', 'result']:
                    if status_key in data:
                        entry["status"] = str(data[status_key])
                        break

                # Try to get schema
                if 'schema' in data:
                    entry["schema"] = data['schema']

                index.append(entry)

            except json.JSONDecodeError as e:
                errors.append({"path": str(rel_path), "error": f"JSON decode error: {e}"})
            except Exception as e:
                errors.append({"path": str(rel_path), "error": str(e)})

    # Sort by timestamp if available
    def sort_key(x):
        ts = x.get("timestamp", "")
        # Handle various timestamp types
        if isinstance(ts, (int, float)):
            return str(ts)
        return str(ts) if ts else "0"

    index.sort(key=sort_key, reverse=True)

    result = {
        "generated_utc": datetime.utcnow().isoformat(),
        "receipts_dir": str(receipts_dir),
        "total_receipts": len(index),
        "parse_errors": len(errors),
        "receipts": index,
    }

    if errors:
        result["errors"] = errors

    if args.output:
        out_path = Path(args.output)
        out_path.write_text(json.dumps(result, indent=2))
        print(f"Wrote index to: {out_path}")
        print(f"Total receipts: {len(index)}")
        if errors:
            print(f"Parse errors: {len(errors)}")
    else:
        print(json.dumps(result, indent=2))

    return 0


def cmd_qubo_planner(args):
    """Batch job scheduling via QUBO optimization."""
    import sys
    base_dir = Path(__file__).parent.parent
    sys.path.insert(0, str(base_dir))

    from flowtorch.planner_router import (
        PlannerRouter, PlannerConfig, PlannerJob,
        generate_synthetic_planner_jobs
    )

    # Parse jobs from file or generate synthetic
    if args.jobs_file:
        jobs_path = Path(args.jobs_file)
        if not jobs_path.exists():
            print(f"ERROR: Jobs file not found: {jobs_path}")
            return 1

        jobs_data = json.loads(jobs_path.read_text())
        jobs = [PlannerJob(**j) for j in jobs_data]
        n_jobs = len(jobs)
        print(f"Loaded {n_jobs} jobs from {jobs_path}")
    else:
        n_jobs = args.n_jobs
        jobs = generate_synthetic_planner_jobs(n_jobs, args.seed)
        print(f"Generated {n_jobs} synthetic jobs (seed={args.seed})")

    # Configure planner
    receipt_dir = Path(args.receipt_dir) if args.receipt_dir else base_dir / "receipts" / "planner"
    config = PlannerConfig(
        receipt_dir=receipt_dir,
        sa_seed=args.seed,
        sa_sweeps=args.sweeps,
        sa_num_reads=args.num_reads,
        brute_force_max_jobs=args.brute_force_max,
        gpu_vram_mb=args.gpu_vram,
        qpu_available=args.qpu,
    )

    planner = PlannerRouter(config)
    planner.queue_jobs(jobs)

    print(f"\n=== QUBO Batch Planner ===")
    print(f"Jobs: {n_jobs}")
    print(f"Solver threshold: brute_force if n_jobs <= {args.brute_force_max}, else SA")
    print()

    # Plan
    print("Planning...")
    result = planner.plan()

    # Results
    print(f"\n=== Results ===")
    print(f"Variables: {result.n_vars} (assignments: {n_jobs * 3}, slack: {result.n_vars - n_jobs * 3})")
    print(f"Solve method: {result.solve_method}")
    print(f"Solve time: {result.solve_time_ms:.2f} ms")
    print(f"Energy: {result.energy:.2f}")
    print(f"Backend distribution: {result.backend_counts}")
    print(f"Constraint violations: {result.constraint_violations}")

    # Verification
    if result.verification:
        v = result.verification
        print(f"\n=== Verification ===")
        print(f"One-hot violations: {v['onehot_violations']}")
        print(f"VRAM used: {v['vram_used_chunks']} chunks ({v['vram_used_mb']:.0f} MB)")
        print(f"VRAM capacity: {v['vram_cap_chunks']} chunks ({v['vram_cap_mb_effective']:.0f} MB effective)")
        print(f"VRAM slack: {v['vram_slack_value']} chunks")
        print(f"VRAM constraint LHS: {v['vram_constraint_lhs']} (should = {v['vram_cap_chunks']})")
        print(f"VRAM constraint satisfied: {v['vram_constraint_satisfied']}")
        print(f"VRAM over capacity: {v['vram_over_capacity']}")

    if not args.quiet:
        print(f"\nAssignments (first 10):")
        for a in result.assignments[:10]:
            print(f"  Job {a['job_id']:2d}: {a['backend']:4s} (Se={a['se']:.2f}, VRAM={a['vram_mb']:.0f}MB)")
        if len(result.assignments) > 10:
            print(f"  ... and {len(result.assignments) - 10} more")

    if result.receipt_path:
        print(f"\nReceipt: {result.receipt_path}")

    # Output full results to JSON if requested
    if args.output:
        output_path = Path(args.output)
        output_data = {
            "n_jobs": result.n_jobs,
            "n_vars": result.n_vars,
            "solve_method": result.solve_method,
            "solve_time_ms": result.solve_time_ms,
            "energy": result.energy,
            "backend_counts": result.backend_counts,
            "constraint_violations": result.constraint_violations,
            "assignments": result.assignments,
        }
        output_path.write_text(json.dumps(output_data, indent=2))
        print(f"Full results: {output_path}")

    return 0


# ============================================
# CDNAv2 Commands
# ============================================

def cmd_cdna_convert_v2(args):
    """Convert CDNA v1 files to v2 format."""
    from pathlib import Path

    # Import converter
    input_path = Path(args.input)
    output_path = Path(args.output) if args.output else None

    if input_path.is_dir():
        # Batch conversion
        from tools.cdna_v1_to_v2_converter import batch_convert, print_summary

        if output_path is None:
            output_path = input_path.parent / f"{input_path.name}_v2"

        output_path.mkdir(parents=True, exist_ok=True)

        receipt = batch_convert(
            input_dir=input_path,
            output_dir=output_path,
            block_rows=args.block_rows,
            verify=args.verify,
            parallel=args.parallel,
            pattern=args.pattern,
        )

        print_summary(receipt)

        # Write receipt
        receipt_path = Path(args.receipt) if args.receipt else Path("receipts/cdna_v2_conversion.json")
        receipt_path.parent.mkdir(parents=True, exist_ok=True)
        receipt_summary = {k: v for k, v in receipt.items() if k != "per_shard_results"}
        receipt_path.write_text(json.dumps(receipt_summary, indent=2))
        print(f"\nReceipt: {receipt_path}")

        return 0 if receipt["error_count"] == 0 else 1

    else:
        # Single file conversion
        from helix_cdc.regrow.cdna_stream_v2 import convert_v1_to_v2

        if output_path is None:
            stem = input_path.stem
            if stem.endswith(".cdna"):
                stem = stem[:-5]
            output_path = input_path.parent / f"{stem}.cdna2.hxz"

        receipt = convert_v1_to_v2(
            v1_path=input_path,
            v2_path=output_path,
            block_rows=args.block_rows,
            verify=args.verify,
        )

        print(f"Converted: {input_path.name} -> {output_path.name}")
        print(f"  V1 size: {receipt['v1_bytes']:,} bytes")
        print(f"  V2 size: {receipt['v2_bytes']:,} bytes")
        print(f"  Diff: {receipt['size_diff_bytes']:+,} bytes")

        if args.verify:
            v = receipt["verification"]
            print(f"  Verification: {'PASS' if v['match'] else 'FAIL'}")

        return 0 if receipt["verification"]["match"] else 1


def cmd_cdna_verify(args):
    """Verify all blocks in a CDNAv2 file."""
    from pathlib import Path
    from helix_cdc.regrow.cdna_stream_v2 import CDNAv2Reader

    path = Path(args.file)
    if not path.exists():
        print(f"ERROR: File not found: {path}")
        return 1

    try:
        reader = CDNAv2Reader(path)
    except ValueError as e:
        print(f"ERROR: {e}")
        return 1

    print(f"Verifying: {path.name}")
    print(f"  Tensor: {reader.tensor_name}")
    print(f"  Shape: {reader.shape}")
    print(f"  Blocks: {reader.num_blocks}")
    print()

    ok_count, fail_count, failed = reader.verify_all_blocks()

    print(f"Block verification:")
    print(f"  OK: {ok_count}/{reader.num_blocks}")
    print(f"  FAILED: {fail_count}")

    if failed:
        print(f"  Failed blocks: {failed}")

    verdict = "PASS" if fail_count == 0 else "FAIL"
    print(f"\nVerdict: {verdict}")

    return 0 if fail_count == 0 else 1


def cmd_cdna_info(args):
    """Show CDNAv2 file metadata."""
    from pathlib import Path
    from helix_cdc.regrow.cdna_stream_v2 import CDNAv2Reader, load_cdna_auto, CDNA2_MAGIC
    from helix_cdc.regrow.cdna_stream import CDNA_MAGIC

    path = Path(args.file)
    if not path.exists():
        print(f"ERROR: File not found: {path}")
        return 1

    # Detect format
    with open(path, "rb") as f:
        magic = f.read(4)

    if magic == CDNA2_MAGIC:
        version = "v2"
        reader = CDNAv2Reader(path)

        info = {
            "path": str(path),
            "format": "CDNAv2",
            "tensor_name": reader.tensor_name,
            "shape": list(reader.shape),
            "rows": reader.rows,
            "cols": reader.cols,
            "block_rows": reader.block_rows,
            "num_blocks": reader.num_blocks,
            "codebook_min": float(reader.codebook.min()),
            "codebook_max": float(reader.codebook.max()),
            "codebook_mean": float(reader.codebook.mean()),
            "file_size_bytes": path.stat().st_size,
        }

        # Add block index info
        info["block_index"] = []
        for i, entry in enumerate(reader.block_index):
            info["block_index"].append({
                "block": i,
                "offset": entry.offset,
                "comp_len": entry.comp_len,
                "decomp_len": entry.decomp_len,
                "sha256": entry.sha256.hex()[:16] + "...",
            })

    elif magic == CDNA_MAGIC:
        version = "v1"
        from helix_cdc.regrow.cdna_stream import inspect_cdna_hxz_header
        info = inspect_cdna_hxz_header(str(path))
        info["format"] = "CDNAv1"

    else:
        print(f"ERROR: Unknown format (magic: {magic!r})")
        return 1

    if args.json:
        print(json.dumps(info, indent=2))
    else:
        print(f"File: {path.name}")
        print(f"Format: {info.get('format', 'unknown')}")
        print(f"Tensor: {info.get('tensor_name', 'unknown')}")
        print(f"Shape: {info.get('shape', [])}")

        if version == "v2":
            print(f"Block rows: {info.get('block_rows', 0)}")
            print(f"Num blocks: {info.get('num_blocks', 0)}")

        print(f"Codebook: min={info.get('codebook_min', 0):.4f}, max={info.get('codebook_max', 0):.4f}, mean={info.get('codebook_mean', 0):.4f}")
        print(f"File size: {info.get('file_size_bytes', 0):,} bytes")

        if args.blocks and version == "v2":
            print(f"\nBlock index:")
            for b in info.get("block_index", [])[:10]:
                print(f"  Block {b['block']:3d}: offset={b['offset']:10,}, comp={b['comp_len']:6,}, decomp={b['decomp_len']:6,}, sha256={b['sha256']}")
            if len(info.get("block_index", [])) > 10:
                print(f"  ... and {len(info['block_index']) - 10} more blocks")

    return 0


def cmd_cdna_benchmark(args):
    """Benchmark CDNA compression/decompression."""
    import time
    from pathlib import Path
    import numpy as np
    from helix_cdc.regrow.cdna_stream_v2 import CDNAv2Reader, stream_cdna2_matmul

    path = Path(args.file)
    if not path.exists():
        print(f"ERROR: File not found: {path}")
        return 1

    print(f"Benchmarking: {path.name}")
    print()

    # Read file
    reader = CDNAv2Reader(path)

    print(f"Tensor: {reader.tensor_name}")
    print(f"Shape: {reader.shape}")
    print(f"Blocks: {reader.num_blocks}")
    print()

    # Benchmark full read
    print("=== Full Read Benchmark ===")
    times = []
    for i in range(args.iterations):
        start = time.perf_counter()
        indices = reader.get_all_indices()
        elapsed = time.perf_counter() - start
        times.append(elapsed)
        if i == 0:
            print(f"  Run {i+1}: {elapsed*1000:.2f}ms")

    avg_read = sum(times) / len(times)
    print(f"  Average: {avg_read*1000:.2f}ms ({reader.rows * reader.cols / avg_read / 1e6:.1f}M elements/sec)")

    # Benchmark random access
    print("\n=== Random Access Benchmark ===")
    np.random.seed(42)
    row_ranges = [(np.random.randint(0, reader.rows - 10), np.random.randint(10, 50)) for _ in range(100)]
    row_ranges = [(start, min(start + length, reader.rows)) for start, length in row_ranges]

    start = time.perf_counter()
    for s, e in row_ranges:
        _ = reader.get_rows(s, e)
    elapsed = time.perf_counter() - start
    print(f"  100 random accesses: {elapsed*1000:.2f}ms ({elapsed/100*1000:.3f}ms avg)")

    # Benchmark matmul
    print("\n=== Matmul Benchmark ===")
    x = np.random.randn(reader.cols).astype(np.float32)

    times = []
    for i in range(args.iterations):
        start = time.perf_counter()
        y, stats = stream_cdna2_matmul(path, x)
        elapsed = time.perf_counter() - start
        times.append(elapsed)
        if i == 0:
            print(f"  Run {i+1}: {elapsed*1000:.2f}ms")

    avg_matmul = sum(times) / len(times)
    flops = 2 * reader.rows * reader.cols  # multiply-add
    print(f"  Average: {avg_matmul*1000:.2f}ms ({flops / avg_matmul / 1e9:.2f} GFLOP/s effective)")

    # Benchmark block verification
    print("\n=== Verification Benchmark ===")
    start = time.perf_counter()
    ok, fail, _ = reader.verify_all_blocks()
    elapsed = time.perf_counter() - start
    print(f"  {reader.num_blocks} blocks verified: {elapsed*1000:.2f}ms ({elapsed/reader.num_blocks*1000:.3f}ms/block)")

    # Summary
    print("\n=== Summary ===")
    file_size = path.stat().st_size
    uncompressed = reader.rows * reader.cols
    ratio = uncompressed / file_size

    print(f"  File size: {file_size:,} bytes")
    print(f"  Uncompressed: {uncompressed:,} bytes (indices only)")
    print(f"  Compression ratio: {ratio:.2f}x")
    print(f"  Memory reduction: ~{stats.get('memory_reduction_factor', 0):.1f}x (vs dense float32)")

    return 0


# ============================================
# CDNA Kernel + Agent Commands (WO-CDNA-KERNEL-V1)
# ============================================

def cmd_cdna_fetch_rows(args):
    """Fetch rows from CDNAv2 file using kernel primitive."""
    import numpy as np
    from pathlib import Path
    from helix_cdc.kernel import CDNAKernel

    path = Path(args.file)
    if not path.exists():
        print(f"ERROR: File not found: {path}")
        return 1

    kernel = CDNAKernel(path)
    end_row = args.end if args.end else kernel.rows

    print(f"Kernel: {kernel}")
    print(f"Fetching rows [{args.start}, {end_row})")
    print(f"Verify: {args.verify}")
    print()

    # Get block mapping
    blocks = kernel.address(args.start, end_row)
    print(f"Blocks needed: {len(blocks)}")
    for bid, ls, le in blocks[:5]:
        sha = kernel.get_block_sha256(bid)
        print(f"  Block {bid}: local [{ls}, {le}), sha256={sha[:16]}...")
    if len(blocks) > 5:
        print(f"  ... and {len(blocks) - 5} more")
    print()

    # Fetch rows
    import time
    t0 = time.perf_counter()
    rows = kernel.get_rows(args.start, end_row, verify=args.verify)
    duration_ms = (time.perf_counter() - t0) * 1000

    print(f"Fetched: {rows.shape}")
    print(f"Duration: {duration_ms:.2f} ms")

    # Save if requested
    if args.output:
        np.save(args.output, rows)
        print(f"Saved to: {args.output}")

    # Receipt
    if args.json:
        import hashlib
        receipt = {
            "schema": "cdna_fetch_rows_v1",
            "file": str(path.resolve()),
            "tensor_name": kernel.tensor_name,
            "shape": list(kernel.shape),
            "row_range": [args.start, end_row],
            "blocks_touched": [b[0] for b in blocks],
            "verified": args.verify,
            "output_shape": list(rows.shape),
            "output_sha256": hashlib.sha256(rows.tobytes()).hexdigest(),
            "duration_ms": duration_ms,
        }
        print()
        print(json.dumps(receipt, indent=2))

    return 0


def cmd_cdna_matmul(args):
    """Compute y = W @ x using CDNA kernel streaming."""
    import numpy as np
    import hashlib
    from pathlib import Path
    from helix_cdc.kernel import CDNAKernel

    path = Path(args.file)
    if not path.exists():
        print(f"ERROR: File not found: {path}")
        return 1

    kernel = CDNAKernel(path)

    print(f"Kernel: {kernel}")
    print(f"W shape: {kernel.shape}")
    print(f"Verify: {args.verify}")
    print()

    # Load or generate input
    if args.input:
        x = np.load(args.input).astype(np.float32).ravel()
        print(f"Input from: {args.input}")
    else:
        np.random.seed(args.seed)
        x = np.random.randn(kernel.cols).astype(np.float32)
        print(f"Input: random (seed={args.seed})")

    if len(x) != kernel.cols:
        print(f"ERROR: Input dimension {len(x)} != W cols {kernel.cols}")
        return 1

    print(f"x shape: {x.shape}, norm: {np.linalg.norm(x):.4f}")
    print()

    # Compute matmul
    y, receipt = kernel.stream_matmul(x, verify=args.verify)

    print(f"y shape: {y.shape}")
    print(f"y norm: {np.linalg.norm(y):.4f}")
    print(f"Duration: {receipt.duration_ms:.2f} ms")
    print(f"Blocks processed: {receipt.extra['blocks_processed']}")

    # Save if requested
    if args.output:
        np.save(args.output, y)
        print(f"Saved to: {args.output}")

    # Receipt
    if args.json:
        full_receipt = {
            "schema": "cdna_matmul_v1",
            "file": str(path.resolve()),
            "tensor_name": kernel.tensor_name,
            "tensor_shape": list(kernel.shape),
            "input_sha256": hashlib.sha256(x.tobytes()).hexdigest(),
            "output_sha256": hashlib.sha256(y.tobytes()).hexdigest(),
            "output_norm": float(np.linalg.norm(y)),
            "verified": args.verify,
            "blocks_processed": receipt.extra['blocks_processed'],
            "blocks_verified": receipt.extra['blocks_verified'],
            "duration_ms": receipt.duration_ms,
        }
        print()
        print(json.dumps(full_receipt, indent=2))

    return 0


def cmd_cdna_cache_stats(args):
    """Show agent cache statistics."""
    import numpy as np
    from pathlib import Path
    from helix_cdc.kernel import CDNAKernel
    from helix_cdc.agents import FibPiChunkAgent

    path = Path(args.file)
    if not path.exists():
        print(f"ERROR: File not found: {path}")
        return 1

    print(f"Testing cache with policy='{args.policy}'")
    print(f"Accesses: {args.accesses}")
    print()

    agent = FibPiChunkAgent(verify_policy=args.policy)
    kernel = agent.open_capsule(path)

    print(f"Kernel: {kernel}")
    print()

    # Random access pattern
    np.random.seed(42)
    for i in range(args.accesses):
        start = np.random.randint(0, max(1, kernel.rows - 16))
        end = min(start + np.random.randint(8, 32), kernel.rows)
        _, receipt = agent.fetch_rows(kernel, start, end)

        if i < 5:
            print(f"  Access {i+1}: rows [{start}, {end}), hit={receipt.cache_hit}")

    if args.accesses > 5:
        print(f"  ... and {args.accesses - 5} more accesses")

    print()
    stats = agent.cache_stats()

    if args.json:
        print(json.dumps(stats, indent=2))
    else:
        print("=== Cache Stats ===")
        print(f"Policy: {stats['policy']}")
        print(f"Total accesses: {stats['access_count']}")
        print(f"Cache hits: {stats['cache_hits']}")
        print(f"Cache misses: {stats['cache_misses']}")
        print(f"Hit rate: {stats['hit_rate']*100:.1f}%")
        print(f"Blocks verified: {stats['blocks_verified']}")
        print(f"Cache entries: {stats['cache']['entries']}")
        print(f"Cache size: {stats['cache']['size_mb']:.2f} MB")

    return 0


def cmd_cdna_kernel_kat(args):
    """Run kernel Known Answer Test (deterministic correctness check)."""
    import numpy as np
    import hashlib
    from pathlib import Path
    from datetime import datetime
    from helix_cdc.kernel import CDNAKernel

    path = Path(args.file)
    if not path.exists():
        print(f"ERROR: File not found: {path}")
        return 1

    kernel = CDNAKernel(path)

    print("=== CDNA Kernel KAT ===")
    print(f"File: {path.resolve()}")
    print(f"Tensor: {kernel.tensor_name}")
    print(f"Shape: {kernel.shape}")
    print(f"Blocks: {kernel.num_blocks} x {kernel.block_rows}")
    print(f"RNG seed: {args.seed}")
    print()

    # Generate deterministic input
    np.random.seed(args.seed)
    x = np.random.randn(kernel.cols).astype(np.float32)
    x_sha256 = hashlib.sha256(x.tobytes()).hexdigest()

    print(f"Input SHA256: {x_sha256[:32]}...")
    print()

    # Verify all blocks first
    print("Verifying all blocks...")
    ok, fail, failed = kernel.verify_all_blocks()
    if fail > 0:
        print(f"ERROR: {fail} blocks failed verification: {failed}")
        return 1
    print(f"  {ok}/{kernel.num_blocks} blocks OK")
    print()

    # Compute matmul with verification
    print("Computing y = W @ x (verified)...")
    y, receipt = kernel.stream_matmul(x, verify=True)
    y_sha256 = hashlib.sha256(y.tobytes()).hexdigest()

    print(f"Output SHA256: {y_sha256[:32]}...")
    print(f"Output norm: {np.linalg.norm(y):.8f}")
    print(f"Duration: {receipt.duration_ms:.2f} ms")
    print()

    # Build receipt
    kat_receipt = {
        "schema": "cdna_kernel_kat_v1",
        "timestamp_utc": datetime.utcnow().isoformat() + "Z",
        "file": str(path.resolve()),
        "tensor_name": kernel.tensor_name,
        "tensor_shape": list(kernel.shape),
        "block_rows": kernel.block_rows,
        "num_blocks": kernel.num_blocks,
        "codebook_sha256": hashlib.sha256(kernel.codebook.tobytes()).hexdigest(),
        "rng_seed": args.seed,
        "input_sha256": x_sha256,
        "output_sha256": y_sha256,
        "output_norm": float(np.linalg.norm(y)),
        "blocks_verified": kernel.num_blocks,
        "duration_ms": receipt.duration_ms,
        "verdict": "PASS",
    }

    print("=== KAT Receipt ===")
    print(json.dumps(kat_receipt, indent=2))

    # Save receipt if requested
    if args.receipt:
        receipt_path = Path(args.receipt)
        receipt_path.parent.mkdir(parents=True, exist_ok=True)
        receipt_path.write_text(json.dumps(kat_receipt, indent=2))
        print(f"\nReceipt saved: {receipt_path}")

    return 0


def cmd_self_explain(args):
    """Generate self-explanation report: what Echo can do and what's proven."""
    import yaml
    from datetime import datetime

    # Find tool_registry.yaml
    base_dir = Path(__file__).parent.parent
    registry_path = base_dir / "tool_registry.yaml"
    taxonomy_path = base_dir / "docs" / "REGEN_TAXONOMY.md"

    report = {
        "generated_utc": datetime.utcnow().isoformat(),
        "echo_version": "helix-cdc CLI",
        "capabilities": {},
        "warnings": [],
    }

    # Load tool registry if exists
    if registry_path.exists():
        try:
            registry = yaml.safe_load(registry_path.read_text())
            report["tool_registry_loaded"] = True

            # Extract CLI commands
            if "cli_commands" in registry:
                cmds = registry["cli_commands"]
                report["capabilities"]["cli_commands"] = {
                    "procedural": [k for k, v in cmds.items() if v.get("type") == "procedural"],
                    "fidelity": [k for k, v in cmds.items() if v.get("type") == "fidelity"],
                    "deprecated": [k for k, v in cmds.items() if v.get("type") == "deprecated"],
                    "utility": [k for k, v in cmds.items() if v.get("type") == "utility"],
                }

            # Extract taxonomy
            if "taxonomy" in registry:
                report["taxonomy"] = registry["taxonomy"]

        except Exception as e:
            report["tool_registry_loaded"] = False
            report["warnings"].append(f"Failed to load tool_registry.yaml: {e}")
    else:
        report["tool_registry_loaded"] = False
        report["warnings"].append(f"tool_registry.yaml not found at {registry_path}")

    # Check regen taxonomy doc
    report["regen_taxonomy_doc"] = str(taxonomy_path) if taxonomy_path.exists() else None

    # Key facts
    report["key_facts"] = {
        "procedural_regen": "Produces valid GGUF container with NOISE weights (SHA256 will NOT match original)",
        "fidelity_regen": "Requires stored residual or CAS vault; produces bit-exact copy (SHA256 WILL match)",
        "deprecated_template_method": "Template library has 0.0002% match rate - DISPROVEN",
        "dna_seed_expansion": "SHAKE256 PRG expansion - DISPROVEN for fidelity (produces different tensors)",
    }

    # Output
    if args.output:
        out_path = Path(args.output)
        out_path.write_text(json.dumps(report, indent=2))
        print(f"Wrote self-explain report to: {out_path}")
    else:
        print(json.dumps(report, indent=2))

    return 0


# ============================================
# Track 1: Productization CLI Commands (2026-01-25)
# ============================================

def cmd_compress(args):
    """Compress a GGUF model to Hybrid CDNA v2 + HXZO sidecars."""
    import subprocess

    gguf_path = Path(args.gguf)
    out_dir = Path(args.out)

    if not gguf_path.exists():
        print(f"ERROR: GGUF file not found: {gguf_path}", file=sys.stderr)
        return 1

    # Create output directories
    cdna_dir = out_dir / "cdna"
    sidecar_dir = out_dir / "sidecars"
    cdna_dir.mkdir(parents=True, exist_ok=True)
    sidecar_dir.mkdir(parents=True, exist_ok=True)

    manifest_path = out_dir / "manifest.json"

    # Call batch converter
    cmd = [
        sys.executable,
        str(Path(__file__).resolve().parents[1] / "tools" / "batch_convert_hybrid_cdna_v2.py"),
        "--source", str(gguf_path),
        "--cdna-dir", str(cdna_dir),
        "--sidecar-dir", str(sidecar_dir),
        "--manifest", str(manifest_path),
        "--percentile", str(args.percentile),
        "--jobs", str(args.jobs),
    ]

    if args.verbose:
        print(f"Running: {' '.join(cmd)}")

    result = subprocess.run(cmd)
    if result.returncode == 0:
        print(f"\n=== Compression complete ===")
        print(f"Manifest: {manifest_path}")
        print(f"CDNA shards: {cdna_dir}")
        print(f"Sidecars: {sidecar_dir}")

    return result.returncode


def cmd_rebuild(args):
    """Rebuild a GGUF from a Hybrid CDNA v2 manifest."""
    import subprocess

    manifest_path = Path(args.manifest)
    output_path = Path(args.out)

    if not manifest_path.exists():
        print(f"ERROR: Manifest not found: {manifest_path}", file=sys.stderr)
        return 1

    # We need a reference GGUF for 1D tensors (norms, etc.)
    # Load manifest to find source
    manifest = json.loads(manifest_path.read_text())
    reference_gguf = manifest.get("source_gguf")

    if args.reference:
        reference_gguf = args.reference
    elif not reference_gguf:
        print("ERROR: No reference GGUF specified and manifest has no source_gguf", file=sys.stderr)
        return 1

    # Call regenerator
    cmd = [
        sys.executable,
        str(Path(__file__).resolve().parents[1] / "tools" / "regen_from_cdna_manifest_canonical_v1.py"),
        "--manifest", str(manifest_path),
        "--reference-gguf", str(reference_gguf),
        "--output", str(output_path),
    ]

    if args.verbose:
        print(f"Running: {' '.join(cmd)}")

    result = subprocess.run(cmd)
    if result.returncode == 0:
        print(f"\n=== Rebuild complete ===")
        print(f"Output: {output_path}")

    return result.returncode


def cmd_verify(args):
    """Verify behavioral equivalence between baseline and rebuilt GGUF."""
    import subprocess
    import tempfile

    baseline_path = Path(args.baseline)
    rebuilt_path = Path(args.rebuilt)

    if not baseline_path.exists():
        print(f"ERROR: Baseline GGUF not found: {baseline_path}", file=sys.stderr)
        return 1
    if not rebuilt_path.exists():
        print(f"ERROR: Rebuilt GGUF not found: {rebuilt_path}", file=sys.stderr)
        return 1

    print("=== Helix-CDC Behavioral Verification ===")
    print(f"Baseline: {baseline_path}")
    print(f"Rebuilt:  {rebuilt_path}")
    print()

    tools_dir = Path(__file__).resolve().parents[1] / "tools"
    tmp_dir = Path(tempfile.gettempdir())

    # Phase 1: Dump baseline logits
    print("[1/3] Dumping baseline logits...")
    baseline_logits = tmp_dir / "verify_baseline_logits.jsonl"
    cmd1 = [
        sys.executable, str(tools_dir / "dump_greedy_logits.py"),
        "--model", str(baseline_path),
        "--output", str(baseline_logits),
        "--max-tokens", str(args.tokens),
        "--teacher",
    ]
    r1 = subprocess.run(cmd1, capture_output=True, text=True)
    if r1.returncode != 0:
        print(f"ERROR: Failed to dump baseline logits: {r1.stderr}", file=sys.stderr)
        return 1

    # Phase 2: Dump rebuilt logits (teacher-forced)
    print("[2/3] Dumping rebuilt logits (teacher-forced)...")
    rebuilt_logits = tmp_dir / "verify_rebuilt_logits.jsonl"
    cmd2 = [
        sys.executable, str(tools_dir / "dump_greedy_logits.py"),
        "--model", str(rebuilt_path),
        "--output", str(rebuilt_logits),
        "--max-tokens", str(args.tokens),
        "--teacher",
        "--teacher-file", str(baseline_logits),
    ]
    r2 = subprocess.run(cmd2, capture_output=True, text=True)
    if r2.returncode != 0:
        print(f"ERROR: Failed to dump rebuilt logits: {r2.stderr}", file=sys.stderr)
        return 1

    # Phase 3: Compare logits
    print("[3/3] Comparing logits...")
    receipt_path = Path(args.receipt) if args.receipt else (tmp_dir / "verify_receipt.json")
    cmd3 = [
        sys.executable, str(tools_dir / "compare_logits_dumps.py"),
        "--baseline", str(baseline_logits),
        "--rebuilt", str(rebuilt_logits),
        "--output", str(receipt_path),
    ]
    r3 = subprocess.run(cmd3, capture_output=True, text=True)
    if r3.returncode != 0:
        print(f"ERROR: Comparison failed: {r3.stderr}", file=sys.stderr)
        return 1

    # Parse and display results
    if receipt_path.exists():
        receipt = json.loads(receipt_path.read_text())
        verdict = receipt.get("verdict", "UNKNOWN")

        print()
        print("=" * 50)
        print(f"VERDICT: {verdict}")
        print("=" * 50)

        metrics = receipt.get("metrics", {})
        print(f"  Teacher in top-100: {metrics.get('teacher_in_top100_pct', 'N/A')}%")
        print(f"  Teacher logit gap (mean): {metrics.get('teacher_gap_mean', 'N/A'):.3f}")
        print(f"  Top-1 agreement: {metrics.get('top1_agreement_pct', 'N/A')}%")
        print(f"  Mean KL: {metrics.get('kl_mean', 'N/A'):.4f}")
        print()
        print(f"Receipt: {receipt_path}")

        if verdict == "PASS" or verdict == "ACCEPTABLE" or "ACCEPTABLE" in verdict:
            return 0
        else:
            return 1
    else:
        print(r3.stdout)
        return r3.returncode


def cmd_attestation_bundle(args):
    """Create a verifiable attestation bundle for ML model provenance.

    Two modes:
      - LITE (default): Small bundle with hashes. External files verified separately.
      - FULL (--include-models): Self-contained. All files copied in. sha256sum -c works.
    """
    import hashlib
    import platform
    import shutil
    from datetime import datetime, timezone

    manifest_path = Path(args.manifest)
    baseline_path = Path(args.baseline)
    rebuilt_path = Path(args.rebuilt)
    output_dir = Path(args.out)

    # Determine bundle mode
    include_models = getattr(args, 'include_models', False)
    copy_shards = getattr(args, 'copy_shards', False)
    bundle_mode = "full" if (include_models or copy_shards) else "lite"

    # Validate inputs
    for p, name in [(manifest_path, "Manifest"), (baseline_path, "Baseline"), (rebuilt_path, "Rebuilt")]:
        if not p.exists():
            print(f"ERROR: {name} not found: {p}", file=sys.stderr)
            return 1

    # Create output directory
    output_dir.mkdir(parents=True, exist_ok=True)

    print("=== Creating Attestation Bundle ===")
    print(f"Mode:     {bundle_mode.upper()}")
    print(f"Manifest: {manifest_path}")
    print(f"Baseline: {baseline_path}")
    print(f"Rebuilt:  {rebuilt_path}")
    print(f"Output:   {output_dir}")
    print()

    # Helper to compute SHA256
    def sha256_file(path: Path) -> str:
        h = hashlib.sha256()
        with path.open("rb") as f:
            for chunk in iter(lambda: f.read(65536), b""):
                h.update(chunk)
        return h.hexdigest()

    # sha256sums only includes files INSIDE the bundle
    sha256sums = []
    # external_hashes tracks files NOT in the bundle (for lite mode)
    external_hashes = {}

    # 1. Copy manifest
    print("[1/6] Copying manifest...")
    dest_manifest = output_dir / "manifest.json"
    shutil.copy2(manifest_path, dest_manifest)
    sha256sums.append((sha256_file(dest_manifest), "manifest.json"))

    # 2. Handle baseline and rebuilt GGUFs
    print("[2/6] Processing model files...")
    baseline_hash = sha256_file(baseline_path)
    rebuilt_hash = sha256_file(rebuilt_path)

    if include_models:
        # FULL mode: copy models into bundle
        print(f"  Copying baseline ({baseline_path.stat().st_size / 1e9:.1f}GB)...")
        baseline_dir = output_dir / "baseline"
        baseline_dir.mkdir(exist_ok=True)
        dest_baseline = baseline_dir / baseline_path.name
        shutil.copy2(baseline_path, dest_baseline)
        sha256sums.append((baseline_hash, f"baseline/{baseline_path.name}"))

        print(f"  Copying rebuilt ({rebuilt_path.stat().st_size / 1e9:.1f}GB)...")
        rebuilt_dir = output_dir / "rebuilt"
        rebuilt_dir.mkdir(exist_ok=True)
        dest_rebuilt = rebuilt_dir / rebuilt_path.name
        shutil.copy2(rebuilt_path, dest_rebuilt)
        sha256sums.append((rebuilt_hash, f"rebuilt/{rebuilt_path.name}"))
    else:
        # LITE mode: just record hashes
        print("  Recording hashes (models not copied - lite mode)")
        external_hashes["baseline"] = {
            "filename": baseline_path.name,
            "sha256": baseline_hash,
            "size_bytes": baseline_path.stat().st_size,
        }
        external_hashes["rebuilt"] = {
            "filename": rebuilt_path.name,
            "sha256": rebuilt_hash,
            "size_bytes": rebuilt_path.stat().st_size,
        }

    # 3. Load manifest and handle CDNA/sidecar files
    manifest = json.loads(manifest_path.read_text())
    cdna_dir = Path(manifest.get("cdna_dir", ""))
    sidecar_dir = Path(manifest.get("sidecar_dir", ""))

    shard_hashes = []
    if args.include_shards or copy_shards:
        print("[3/6] Processing CDNA shards and sidecars...")
        bundle_cdna_dir = output_dir / "cdna"
        bundle_sidecar_dir = output_dir / "sidecars"

        if copy_shards:
            bundle_cdna_dir.mkdir(exist_ok=True)
            bundle_sidecar_dir.mkdir(exist_ok=True)

        for shard in manifest.get("shards", []):
            cdna_path = cdna_dir / Path(shard["path"]).name if cdna_dir.exists() else Path(shard["path"])
            if cdna_path.exists():
                h = sha256_file(cdna_path)
                if copy_shards:
                    shutil.copy2(cdna_path, bundle_cdna_dir / cdna_path.name)
                    sha256sums.append((h, f"cdna/{cdna_path.name}"))
                else:
                    shard_hashes.append({"path": f"cdna/{cdna_path.name}", "sha256": h})

            sidecar_relpath = shard.get("outlier_sidecar_path")
            if sidecar_relpath:
                sidecar_path = sidecar_dir / sidecar_relpath if sidecar_dir.exists() else Path(sidecar_relpath)
                if sidecar_path.exists():
                    h = sha256_file(sidecar_path)
                    if copy_shards:
                        shutil.copy2(sidecar_path, bundle_sidecar_dir / sidecar_path.name)
                        sha256sums.append((h, f"sidecars/{sidecar_path.name}"))
                    else:
                        shard_hashes.append({"path": f"sidecars/{sidecar_path.name}", "sha256": h})

        if shard_hashes:
            external_hashes["shards"] = shard_hashes
    else:
        print("[3/6] Skipping shards (use --include-shards or --copy-shards)...")

    # 4. Run or copy verification receipt
    print("[4/6] Getting verification receipt...")
    verify_receipt_path = output_dir / "verify_receipt.json"
    if args.verify_receipt and Path(args.verify_receipt).exists():
        shutil.copy2(args.verify_receipt, verify_receipt_path)
    else:
        # Try to use existing logits dumps
        import subprocess
        import tempfile

        tools_dir = Path(__file__).resolve().parents[1] / "tools"
        tmp_dir = Path(tempfile.gettempdir())

        existing_baseline = tmp_dir / "verify_baseline_logits.jsonl"
        existing_rebuilt = tmp_dir / "verify_rebuilt_logits.jsonl"

        if existing_baseline.exists() and existing_rebuilt.exists():
            cmd = [
                sys.executable, str(tools_dir / "compare_logits_dumps.py"),
                "--baseline", str(existing_baseline),
                "--rebuilt", str(existing_rebuilt),
                "--receipt", str(verify_receipt_path),
            ]
            subprocess.run(cmd, capture_output=True)
        else:
            print("  (No existing logits dumps - creating minimal receipt)")
            verify_receipt = {
                "note": "Verification not run - use 'helix verify' for full behavioral test",
                "manifest_stats": manifest.get("stats", {}),
            }
            verify_receipt_path.write_text(json.dumps(verify_receipt, indent=2))

    if verify_receipt_path.exists():
        sha256sums.append((sha256_file(verify_receipt_path), "verify_receipt.json"))

    # 5. Create tool_versions.json
    print("[5/6] Creating tool versions...")
    tool_versions = {
        "python_version": platform.python_version(),
        "platform": platform.platform(),
        "helix_version": "0.2.0",
        "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
    }

    # Try to get git commit
    try:
        import subprocess
        result = subprocess.run(
            ["git", "rev-parse", "HEAD"],
            capture_output=True, text=True, cwd=Path(__file__).parent
        )
        if result.returncode == 0:
            tool_versions["git_commit"] = result.stdout.strip()
    except Exception:
        pass

    tool_versions_path = output_dir / "tool_versions.json"
    tool_versions_path.write_text(json.dumps(tool_versions, indent=2))
    sha256sums.append((sha256_file(tool_versions_path), "tool_versions.json"))

    # 6. Create metadata.json (high-level summary with verification commands)
    print("[6/6] Creating metadata...")
    verify_receipt_data = {}
    if verify_receipt_path.exists():
        try:
            verify_receipt_data = json.loads(verify_receipt_path.read_text())
        except Exception:
            pass

    # Build verification commands based on mode
    if bundle_mode == "full":
        verification_commands = [
            "cd <bundle_dir> && sha256sum -c sha256sums.txt",
        ]
    else:
        verification_commands = [
            "# Verify in-bundle files:",
            "cd <bundle_dir> && sha256sum -c sha256sums.txt",
            "",
            "# Verify external files (baseline + rebuilt):",
            f"sha256sum <baseline_path> | grep {baseline_hash}",
            f"sha256sum <rebuilt_path> | grep {rebuilt_hash}",
            "",
            "# Or use helix verify-external:",
            "python3 -m helix_cdc verify-external --bundle <bundle_dir> --baseline <path> --rebuilt <path>",
        ]

    metadata = {
        "claim": "Verified ML Model Compression",
        "bundle_mode": bundle_mode,
        "compression_ratio": manifest.get("stats", {}).get("compression_ratio", 2.12),
        "behavioral_verdict": verify_receipt_data.get("verdict", "NOT_RUN"),
        "fidelity_outlier_fraction": manifest.get("stats", {}).get("outlier_fraction", 0),
        "inputs": {
            "baseline": {
                "filename": baseline_path.name,
                "sha256": baseline_hash,
                "size_bytes": baseline_path.stat().st_size,
                "included_in_bundle": include_models,
            },
            "rebuilt": {
                "filename": rebuilt_path.name,
                "sha256": rebuilt_hash,
                "size_bytes": rebuilt_path.stat().st_size,
                "included_in_bundle": include_models,
            },
        },
        "verification_commands": verification_commands,
        "created_utc": tool_versions["timestamp"],
        "created_by": "helix-cdc attestation-bundle",
    }
    metadata_path = output_dir / "metadata.json"
    metadata_path.write_text(json.dumps(metadata, indent=2))
    sha256sums.append((sha256_file(metadata_path), "metadata.json"))

    # Write external_hashes.json for lite mode
    if external_hashes:
        external_hashes_path = output_dir / "external_hashes.json"
        external_hashes_path.write_text(json.dumps(external_hashes, indent=2))
        sha256sums.append((sha256_file(external_hashes_path), "external_hashes.json"))

    # Write sha256sums.txt (only in-bundle files)
    sha256sums_path = output_dir / "sha256sums.txt"
    with sha256sums_path.open("w") as f:
        for hash_val, path in sha256sums:
            f.write(f"{hash_val}  {path}\n")

    print()
    print(f"=== Attestation Bundle Created ({bundle_mode.upper()} mode) ===")
    print(f"Location: {output_dir}")
    print()
    print("Contents:")
    for _, path in sha256sums:
        print(f"  {path}")
    print(f"  sha256sums.txt ({len(sha256sums)} entries)")

    if bundle_mode == "lite":
        print()
        print("External files (not in bundle, verify separately):")
        print(f"  baseline: {baseline_path.name} ({baseline_hash[:16]}...)")
        print(f"  rebuilt:  {rebuilt_path.name} ({rebuilt_hash[:16]}...)")

    print()
    print("Verification:")
    if bundle_mode == "full":
        print(f"  cd {output_dir} && sha256sum -c sha256sums.txt")
    else:
        print(f"  # In-bundle files:")
        print(f"  cd {output_dir} && sha256sum -c sha256sums.txt")
        print()
        print(f"  # External files (compare to external_hashes.json):")
        print(f"  sha256sum {baseline_path} {rebuilt_path}")

    return 0


# ============================================
# FGIP Causality Engine Commands
# ============================================

def cmd_fgip_init_db(args):
    """Initialize FGIP causality database."""
    from pathlib import Path
    from helix_cdc.fgip import CausalityEngine
    from helix_cdc.fgip.loader import load_seeds

    db_path = Path(args.db)
    sources_dir = Path(args.sources_dir)
    receipts_dir = db_path.parent / "receipts"

    print(f"=== FGIP Database Initialization ===")
    print(f"Database: {db_path}")
    print(f"Sources:  {sources_dir}")
    print()

    with CausalityEngine(db_path, sources_dir, receipts_dir) as engine:
        receipt = engine.init_db()
        print(f"Schema version: {receipt['schema_version']}")
        print(f"Tables created: {len(receipt['tables_created'])}")

        # Load seeds if specified
        if args.seed_dir:
            seed_dir = Path(args.seed_dir)
            print(f"\nLoading seeds from: {seed_dir}")
            result = load_seeds(engine, seed_dir)
            print(f"  Nodes loaded: {result['nodes_loaded']}")
            print(f"  Edges loaded: {result['edges_loaded']}")
            if result['errors']:
                for err in result['errors']:
                    print(f"  Warning: {err}")

        stats = engine.get_stats()
        print(f"\nDatabase stats:")
        print(f"  Nodes: {stats['total_nodes']}")
        print(f"  Edges: {stats['total_edges']}")

    print(f"\nReceipt: {receipt['receipt_id']}")
    return 0


def cmd_fgip_load_seed(args):
    """Load seed data into FGIP database."""
    from pathlib import Path
    from helix_cdc.fgip import CausalityEngine
    from helix_cdc.fgip.loader import load_seed_nodes, load_seed_edges

    db_path = Path(args.db)
    sources_dir = Path(args.sources_dir)

    if not db_path.exists():
        print(f"ERROR: Database not found: {db_path}")
        print("Run 'fgip-init-db' first.")
        return 1

    print(f"=== FGIP Seed Loading ===")
    print(f"Database: {db_path}")
    print()

    with CausalityEngine(db_path, sources_dir) as engine:
        # Load nodes
        nodes_path = Path(args.nodes)
        print(f"Loading nodes from: {nodes_path}")
        count, ids = load_seed_nodes(engine, nodes_path)
        print(f"  Loaded: {count} nodes")

        # Load edges
        edges_path = Path(args.edges)
        print(f"\nLoading edges from: {edges_path}")
        count, ids = load_seed_edges(engine, edges_path)
        print(f"  Loaded: {count} edges")

        stats = engine.get_stats()
        print(f"\nDatabase stats:")
        print(f"  Total nodes: {stats['total_nodes']}")
        print(f"  Total edges: {stats['total_edges']}")
        print(f"  Edges by status: {stats['edges_by_status']}")

    return 0


def cmd_fgip_add_node(args):
    """Add a node to the FGIP graph."""
    from pathlib import Path
    from helix_cdc.fgip import CausalityEngine

    db_path = Path(args.db)

    if not db_path.exists():
        print(f"ERROR: Database not found: {db_path}")
        return 1

    with CausalityEngine(db_path, db_path.parent / "sources") as engine:
        node_id = engine.add_node(args.id, args.type, args.name)
        print(f"Added node: {node_id}")

    return 0


def cmd_fgip_add_edge(args):
    """Add an edge to the FGIP graph."""
    from pathlib import Path
    from helix_cdc.fgip import CausalityEngine
    from helix_cdc.fgip.models import Sensitivity, AssertionLevel, Visibility

    db_path = Path(args.db)
    sources_dir = Path(args.sources_dir)

    if not db_path.exists():
        print(f"ERROR: Database not found: {db_path}")
        return 1

    sensitivity = Sensitivity[args.sensitivity]
    assertion_level = AssertionLevel[args.assertion_level]
    visibility = Visibility[args.visibility]

    with CausalityEngine(db_path, sources_dir) as engine:
        edge_id = engine.add_edge(
            edge_type=args.type,
            from_node=args.from_node,
            to_node=args.to_node,
            sensitivity=sensitivity,
            assertion_level=assertion_level,
            visibility=visibility,
        )
        print(f"Added edge: {edge_id}")
        print(f"  Type: {args.type}")
        print(f"  From: {args.from_node}")
        print(f"  To:   {args.to_node}")
        print(f"  Assertion: {assertion_level.value}")
        print(f"  Visibility: {visibility.value}")

        # Attach source if provided
        if args.source_url or args.source_file:
            receipt = engine.attach_source(
                edge_id,
                url=args.source_url,
                file=Path(args.source_file) if args.source_file else None,
            )
            print(f"\nSource attached: {receipt['details'].get('source_id')}")
            if receipt['details'].get('promoted_to_evidenced'):
                print("  Status promoted to EVIDENCED")

    return 0


def cmd_fgip_attach_source(args):
    """Attach a source to an existing edge."""
    from pathlib import Path
    from helix_cdc.fgip import CausalityEngine

    db_path = Path(args.db)
    sources_dir = Path(args.sources_dir)

    if not db_path.exists():
        print(f"ERROR: Database not found: {db_path}")
        return 1

    if not args.url and not args.file:
        print("ERROR: Must provide --url or --file")
        return 1

    with CausalityEngine(db_path, sources_dir) as engine:
        receipt = engine.attach_source(
            args.edge_id,
            url=args.url,
            file=Path(args.file) if args.file else None,
            claim_excerpt=args.excerpt,
            page_ref=args.page,
        )
        print(f"Source attached to: {args.edge_id}")
        print(f"  Source ID: {receipt['details'].get('source_id')}")
        print(f"  SHA256: {receipt['details'].get('sha256', 'N/A')[:16]}...")
        if receipt['details'].get('promoted_to_evidenced'):
            print("  Status promoted to EVIDENCED")

    return 0


def cmd_fgip_path(args):
    """Trace causal paths between nodes."""
    import json
    import sqlite3
    from pathlib import Path
    from helix_cdc.fgip.queries import trace_causality

    db_path = Path(args.db)

    if not db_path.exists():
        print(f"ERROR: Database not found: {db_path}")
        return 1

    conn = sqlite3.connect(str(db_path))
    conn.row_factory = sqlite3.Row

    paths = trace_causality(
        conn,
        args.from_node,
        args.to_node,
        max_depth=args.max_depth,
    )

    conn.close()

    if args.json:
        results = []
        for p in paths:
            results.append({
                "path": p.path,
                "edges": p.edges,
                "length": p.total_length,
                "source_coverage": p.source_coverage,
                "evidenced_count": p.evidenced_count,
                "claimed_count": p.claimed_count,
            })
        print(json.dumps(results, indent=2))
    else:
        print(f"=== Causal Paths: {args.from_node} -> {args.to_node} ===")
        print()

        if not paths:
            print("No paths found.")
        else:
            for i, p in enumerate(paths):
                print(f"Path {i+1}:")
                print(f"  Nodes: {' -> '.join(p.path)}")
                print(f"  Length: {p.total_length} edges")
                print(f"  Source coverage: {p.source_coverage*100:.1f}%")
                print(f"  Evidenced: {p.evidenced_count}, Claimed: {p.claimed_count}")
                print()

    return 0


def cmd_fgip_ownership_loop(args):
    """Detect ownership cycles."""
    import json
    import sqlite3
    from pathlib import Path
    from helix_cdc.fgip.queries import ownership_loop

    db_path = Path(args.db)

    if not db_path.exists():
        print(f"ERROR: Database not found: {db_path}")
        return 1

    conn = sqlite3.connect(str(db_path))
    conn.row_factory = sqlite3.Row

    result = ownership_loop(conn, args.node)
    conn.close()

    if args.json:
        print(json.dumps({
            "has_cycle": result.has_cycle,
            "cycle_path": result.cycle_path,
            "cycle_edges": result.cycle_edges,
            "cycle_length": result.cycle_length,
        }, indent=2))
    else:
        print(f"=== Ownership Loop Check: {args.node} ===")
        print()
        if result.has_cycle:
            print("CYCLE DETECTED!")
            print(f"  Path: {' -> '.join(result.cycle_path)}")
            print(f"  Length: {result.cycle_length} edges")
        else:
            print("No ownership cycles found.")

    return 0


def cmd_fgip_contradictions(args):
    """Check for contradictions."""
    import json
    import sqlite3
    from pathlib import Path
    from helix_cdc.fgip.queries import contradiction_check

    db_path = Path(args.db)

    if not db_path.exists():
        print(f"ERROR: Database not found: {db_path}")
        return 1

    conn = sqlite3.connect(str(db_path))
    conn.row_factory = sqlite3.Row

    results = contradiction_check(conn, args.entity)
    conn.close()

    if args.json:
        output = []
        for r in results:
            output.append({
                "entity": r.entity,
                "contradictions": r.contradictions,
                "warning_count": r.warning_count,
                "error_count": r.error_count,
            })
        print(json.dumps(output, indent=2))
    else:
        print(f"=== Contradiction Check ===")
        print()

        if not results:
            print("No contradictions found.")
        else:
            for r in results:
                print(f"Entity: {r.entity}")
                print(f"  Warnings: {r.warning_count}, Errors: {r.error_count}")
                for c in r.contradictions:
                    severity = c['severity']
                    print(f"  [{severity}] {c['type']}: {c['message']}")
                print()

    return 0


def cmd_fgip_correction_score(args):
    """Compute correction score for an entity."""
    import json
    import sqlite3
    from pathlib import Path
    from helix_cdc.fgip.queries import correction_score

    db_path = Path(args.db)

    if not db_path.exists():
        print(f"ERROR: Database not found: {db_path}")
        return 1

    conn = sqlite3.connect(str(db_path))
    conn.row_factory = sqlite3.Row

    result = correction_score(conn, args.entity)
    conn.close()

    if args.json:
        print(json.dumps({
            "entity": result.entity,
            "score": result.score,
            "total_edges": result.total_edges,
            "evidenced_edges": result.evidenced_edges,
            "claimed_edges": result.claimed_edges,
            "disputed_edges": result.disputed_edges,
            "retracted_edges": result.retracted_edges,
            "source_coverage": result.source_coverage,
            "tier_breakdown": result.tier_breakdown,
            "audit_trail": result.audit_trail,
        }, indent=2))
    else:
        print(f"=== Correction Score: {args.entity} ===")
        print()
        print(f"Score: {result.score:.2%}")
        print()
        print(f"Edge breakdown:")
        print(f"  Total:     {result.total_edges}")
        print(f"  Evidenced: {result.evidenced_edges}")
        print(f"  Claimed:   {result.claimed_edges}")
        print(f"  Disputed:  {result.disputed_edges}")
        print(f"  Retracted: {result.retracted_edges}")
        print()
        print(f"Source coverage: {result.source_coverage:.1%}")
        if result.tier_breakdown:
            print(f"Source tiers: {result.tier_breakdown}")

    return 0


def cmd_fgip_export(args):
    """Export the FGIP graph."""
    import sqlite3
    from pathlib import Path
    from helix_cdc.fgip.models import Sensitivity
    from helix_cdc.fgip.export import export_graph, export_cytoscape

    db_path = Path(args.db)

    if not db_path.exists():
        print(f"ERROR: Database not found: {db_path}")
        return 1

    conn = sqlite3.connect(str(db_path))
    conn.row_factory = sqlite3.Row

    output_path = Path(args.output)
    max_sensitivity = Sensitivity[args.max_sensitivity]
    public_mode = getattr(args, 'public', False)

    if args.format == "json":
        result = export_graph(
            conn,
            output_path,
            max_sensitivity=max_sensitivity,
            public_mode=public_mode,
        )
    elif args.format == "cytoscape":
        result = export_cytoscape(conn, output_path)

    conn.close()

    print(f"Exported to: {output_path}")
    print(f"  Mode: {'PUBLIC' if public_mode else 'PRIVATE'}")
    print(f"  Nodes: {result.get('statistics', {}).get('nodes', len(result.get('elements', [])))}")
    print(f"  Edges: {result.get('statistics', {}).get('edges', 0)}")

    if public_mode and 'public_mode_filters' in result:
        filters = result['public_mode_filters']
        print(f"  Filtered out:")
        print(f"    HYPOTHESIS: {filters.get('skipped_hypothesis', 0)}")
        print(f"    Not EVIDENCED: {filters.get('skipped_not_evidenced', 0)}")
        print(f"    PRIVATE visibility: {filters.get('skipped_private_visibility', 0)}")

    return 0


def cmd_fgip_stats(args):
    """Show FGIP database statistics."""
    import json
    from pathlib import Path
    from helix_cdc.fgip import CausalityEngine

    db_path = Path(args.db)

    if not db_path.exists():
        print(f"ERROR: Database not found: {db_path}")
        return 1

    with CausalityEngine(db_path, db_path.parent / "sources") as engine:
        stats = engine.get_stats()
        version = engine.get_schema_version()

    if args.json:
        stats['schema_version'] = version
        print(json.dumps(stats, indent=2))
    else:
        print(f"=== FGIP Database Statistics ===")
        print(f"Database: {db_path}")
        print(f"Schema version: {version}")
        print()
        print(f"Nodes: {stats['total_nodes']}")
        if stats.get('nodes_by_type'):
            for t, c in stats['nodes_by_type'].items():
                print(f"  {t}: {c}")
        print()
        print(f"Edges: {stats['total_edges']}")
        if stats.get('edges_by_status'):
            for s, c in stats['edges_by_status'].items():
                print(f"  {s}: {c}")
        print()
        print(f"Sources: {stats['total_sources']}")
        print(f"  With SHA256: {stats['sources_with_sha256']}")
        print()
        print(f"Claims: {stats['total_claims']}")

    return 0


def cmd_fgip_lint(args):
    """Lint text for lawsuit-inviting phrases."""
    from pathlib import Path
    from helix_cdc.fgip.narrative import detect_catnip, sanitize_text
    from helix_cdc.fgip.models import CATNIP_PHRASES, SAFE_REPLACEMENTS

    # Get text from argument or file
    if args.text:
        text = args.text
    elif args.file:
        text = Path(args.file).read_text()
    else:
        print("ERROR: Must provide text or --file")
        return 1

    violations = detect_catnip(text)

    if not violations:
        print("No catnip phrases detected. Text is clean.")
        return 0

    print(f"=== Catnip Detection: {len(violations)} violations ===")
    print()

    for i, v in enumerate(violations, 1):
        print(f"{i}. '{v.phrase}' at position {v.position}")
        print(f"   Context: {v.context}")
        print(f"   Replace with: {v.suggested_replacement}")
        print()

    if args.fix:
        sanitized, _ = sanitize_text(text)
        print("=== Sanitized text ===")
        print(sanitized)

    return 0


def cmd_fgip_narrative(args):
    """Generate forensic narrative for an edge."""
    from pathlib import Path
    from helix_cdc.fgip import CausalityEngine
    from helix_cdc.fgip.models import Tone
    from helix_cdc.fgip.narrative import generate_edge_narrative

    db_path = Path(args.db)
    sources_dir = Path(args.sources_dir)

    if not db_path.exists():
        print(f"ERROR: Database not found: {db_path}")
        return 1

    tone = Tone[args.tone.upper()]

    with CausalityEngine(db_path, sources_dir) as engine:
        edge = engine.get_edge(args.edge_id)
        if not edge:
            print(f"ERROR: Edge not found: {args.edge_id}")
            return 1

        # Get node names
        from_node = engine.get_node(edge.from_node_id)
        to_node = engine.get_node(edge.to_node_id)

        from_name = from_node.name if from_node else edge.from_node_id
        to_name = to_node.name if to_node else edge.to_node_id

        # Get sources
        sources = []
        for source_id in edge.source_ids:
            src = engine.get_source(source_id)
            if src:
                sources.append(src)

        result = generate_edge_narrative(edge, from_name, to_name, sources, tone)

    print(f"=== Narrative ({tone.value} tone) ===")
    print()
    print(result.text)
    print()
    print(f"Assertion level: {result.assertion_level.value}")
    print(f"Sources cited: {len(result.sources_cited)}")
    print(f"Safe for public: {result.is_safe_for_public}")

    if result.catnip_violations:
        print()
        print(f"WARNING: {len(result.catnip_violations)} catnip phrases detected!")
        for v in result.catnip_violations:
            print(f"  - '{v.phrase}': {v.suggested_replacement}")

    return 0


def main():
    parser = argparse.ArgumentParser(
        prog="helix-cdc",
        description="Helix-CDC: Compressed-Domain Computing toolkit"
    )
    subparsers = parser.add_subparsers(dest="command", help="Available commands")

    # validate command
    p_validate = subparsers.add_parser("validate", help="Validate a receipt")
    p_validate.add_argument("receipt", help="Path to receipt JSON")
    p_validate.set_defaults(func=cmd_validate)

    # ============================================
    # Track 1: Productization Commands (2026-01-25)
    # compress, rebuild, verify - the proven pipeline
    # ============================================

    # compress command - GGUF to Hybrid CDNA v2
    p_compress = subparsers.add_parser(
        "compress",
        help="Compress GGUF to Hybrid CDNA v2 + HXZO sidecars (PROVEN: 2.12x compression)",
        description="""
Compress a GGUF model to Hybrid CDNA v2 format with HXZO outlier sidecars.

PROVEN COMPRESSION (2026-01-24):
  - Compression ratio: ~2.12x (14GB F16 -> 6.6GB CDNA + 34MB sidecars)
  - Fidelity: max error < 0.001 with sidecars
  - Behavioral equivalence: Teacher 100% in top-K

This wraps tools/batch_convert_hybrid_cdna_v2.py for convenience.

Example:
  python -m helix_cdc compress \\
      --gguf model.gguf \\
      --out seeds/my_model/

Output structure:
  seeds/my_model/
    manifest.json        # Full manifest with tensor metadata
    cdna/                # CDNA shards (.cdna.hxz files)
    sidecars/            # HXZO outlier sidecars (.hxzo files)
        """
    )
    p_compress.add_argument(
        "--gguf", "-i",
        required=True,
        help="Input GGUF file to compress"
    )
    p_compress.add_argument(
        "--out", "-o",
        required=True,
        help="Output directory for seeds"
    )
    p_compress.add_argument(
        "--percentile",
        type=float,
        default=99.9,
        help="Percentile for outlier threshold (default: 99.9)"
    )
    p_compress.add_argument(
        "--jobs", "-j",
        type=int,
        default=4,
        help="Parallel workers (default: 4)"
    )
    p_compress.add_argument(
        "--verbose", "-v",
        action="store_true",
        help="Verbose output"
    )
    p_compress.set_defaults(func=cmd_compress)

    # rebuild command - Hybrid CDNA v2 to GGUF
    p_rebuild = subparsers.add_parser(
        "rebuild",
        help="Rebuild GGUF from Hybrid CDNA v2 manifest (PROVEN: 291/291 tensors)",
        description="""
Rebuild a complete GGUF model from a Hybrid CDNA v2 manifest.

PROVEN REBUILD (2026-01-24):
  - 291/291 tensors reconstructed (226 from CDNA + 65 1D from reference)
  - Functional equivalence: Paris ✓, H2O ✓, 1945 ✓
  - Behavioral equivalence: Teacher 100% in top-K

This wraps tools/regen_from_cdna_manifest_canonical_v1.py.

Note: 1D tensors (layer norms, biases) require a reference GGUF.
If the manifest has source_gguf recorded, it will be used automatically.

Example:
  python -m helix_cdc rebuild \\
      --manifest seeds/my_model/manifest.json \\
      --out rebuilt.gguf

  # With explicit reference for 1D tensors:
  python -m helix_cdc rebuild \\
      --manifest seeds/my_model/manifest.json \\
      --reference original.gguf \\
      --out rebuilt.gguf
        """
    )
    p_rebuild.add_argument(
        "--manifest", "-m",
        required=True,
        help="Path to Hybrid CDNA v2 manifest JSON"
    )
    p_rebuild.add_argument(
        "--out", "-o",
        required=True,
        help="Output GGUF path"
    )
    p_rebuild.add_argument(
        "--reference", "-r",
        help="Reference GGUF for 1D tensors (optional if manifest has source_gguf)"
    )
    p_rebuild.add_argument(
        "--verbose", "-v",
        action="store_true",
        help="Verbose output"
    )
    p_rebuild.set_defaults(func=cmd_rebuild)

    # verify command - Behavioral equivalence check
    p_verify = subparsers.add_parser(
        "verify",
        help="Verify behavioral equivalence between baseline and rebuilt GGUF",
        description="""
Verify behavioral equivalence using teacher-forced logit comparison.

METHOD: Two-Phase Teacher-Forced Evaluation
  1. Dump greedy decoding logits from baseline
  2. Dump logits from rebuilt model, forcing it to follow baseline's token choices
  3. Compare logit distributions: KL divergence, teacher rank, top-K agreement

THRESHOLDS (from 2026-01-24 validation):
  - Teacher in top-100: >= 99% (we got 100%)
  - Teacher logit gap (mean): small (we got 0.36)
  - Mean KL: < 0.5 (we got 0.43)

This wraps tools/dump_greedy_logits.py and tools/compare_logits_dumps.py.

Example:
  python -m helix_cdc verify \\
      --baseline original.gguf \\
      --rebuilt rebuilt.gguf

  # With more tokens:
  python -m helix_cdc verify \\
      --baseline original.gguf \\
      --rebuilt rebuilt.gguf \\
      --tokens 64 \\
      --receipt receipts/verify_20260125.json
        """
    )
    p_verify.add_argument(
        "--baseline", "-b",
        required=True,
        help="Path to baseline (original) GGUF"
    )
    p_verify.add_argument(
        "--rebuilt", "-r",
        required=True,
        help="Path to rebuilt GGUF"
    )
    p_verify.add_argument(
        "--tokens", "-t",
        type=int,
        default=32,
        help="Tokens per prompt for verification (default: 32)"
    )
    p_verify.add_argument(
        "--receipt",
        help="Output receipt path (default: temp file)"
    )
    p_verify.set_defaults(func=cmd_verify)

    # attestation-bundle command - Create verifiable provenance package
    p_attest = subparsers.add_parser(
        "attestation-bundle",
        help="Create verifiable attestation bundle for ML model provenance",
        description="""
Create an attestation bundle for ML model provenance verification.

TWO MODES:

  LITE (default): Small bundle with metadata + hashes.
    - sha256sums.txt only covers IN-BUNDLE files
    - external_hashes.json records hashes for baseline/rebuilt GGUFs
    - Verify in-bundle: sha256sum -c sha256sums.txt
    - Verify external: compare sha256sum output to external_hashes.json

  FULL (--include-models): Self-contained bundle.
    - Copies baseline + rebuilt GGUFs into bundle
    - sha256sum -c sha256sums.txt verifies EVERYTHING
    - Portable for airgapped environments / auditors

Bundle contents:
  - manifest.json: CDNA manifest with tensor metadata
  - sha256sums.txt: Integrity hashes for IN-BUNDLE files only
  - external_hashes.json: Hashes for external files (lite mode)
  - verify_receipt.json: Behavioral equivalence proof
  - tool_versions.json: Environment for reproducibility
  - metadata.json: Claim summary + verification commands

See docs/ATTESTATION_BUNDLE.md for full specification.

Examples:
  # Lite bundle (default):
  python -m helix_cdc attestation-bundle \\
      --manifest seeds/hybrid_manifest_v2.json \\
      --baseline tmp/mistral_fp8combined_canonical.gguf \\
      --rebuilt tmp/rebuilt_from_hybrid_cdna_v2.gguf \\
      --out attestation_bundle/

  # Full bundle (self-contained):
  python -m helix_cdc attestation-bundle \\
      --manifest seeds/hybrid_manifest_v2.json \\
      --baseline tmp/mistral_fp8combined_canonical.gguf \\
      --rebuilt tmp/rebuilt_from_hybrid_cdna_v2.gguf \\
      --include-models \\
      --out full_bundle/
        """
    )
    p_attest.add_argument(
        "--manifest", "-m",
        required=True,
        help="Path to Hybrid CDNA v2 manifest JSON"
    )
    p_attest.add_argument(
        "--baseline", "-b",
        required=True,
        help="Path to baseline (original) GGUF"
    )
    p_attest.add_argument(
        "--rebuilt", "-r",
        required=True,
        help="Path to rebuilt GGUF"
    )
    p_attest.add_argument(
        "--out", "-o",
        required=True,
        help="Output directory for attestation bundle"
    )
    p_attest.add_argument(
        "--verify-receipt",
        help="Path to existing verify receipt (skips re-verification)"
    )
    p_attest.add_argument(
        "--include-models",
        action="store_true",
        help="FULL mode: Copy baseline + rebuilt GGUFs into bundle (makes sha256sum -c work universally)"
    )
    p_attest.add_argument(
        "--include-shards",
        action="store_true",
        help="Hash CDNA shards and sidecars (hashes only, not copied)"
    )
    p_attest.add_argument(
        "--copy-shards",
        action="store_true",
        help="FULL mode: Copy CDNA shards and sidecars into bundle"
    )
    p_attest.set_defaults(func=cmd_attestation_bundle)

    # regen-procedural command (PROCEDURAL = valid container, noise weights)
    p_regen = subparsers.add_parser(
        "regen-procedural",
        help="Procedural GGUF generation (valid container, NOISE weights - not original model)",
        description="""
PROCEDURAL Regeneration - produces valid GGUF container with deterministic NOISE weights.

THIS DOES NOT RECOVER THE ORIGINAL MODEL WEIGHTS.
Output SHA256 WILL NOT match original. Use fidelity-* commands for exact recovery.

See docs/REGEN_TAXONOMY.md for the distinction between procedural and fidelity regen.

Example:
  python -m helix_cdc regen-procedural \\
    --dna 1f647d91f6 \\
    --entropy bf690b54 \\
    --header-source /path/to/reference.gguf \\
    --output /path/to/regenerated.gguf \\
    --allow-procedural
        """
    )
    p_regen.add_argument("--dna", required=True, help="DNA seed hex (e.g., 1f647d91f6)")
    p_regen.add_argument("--entropy", required=True, help="Entropy seed hex (e.g., bf690b54)")
    p_regen.add_argument("--atlas", help="Path to centroid atlas .npy (default: package atlas)")
    p_regen.add_argument("--header-source", required=True, help="Reference GGUF for header extraction")
    p_regen.add_argument("--output", required=True, help="Output GGUF path")
    p_regen.add_argument("--slice-mb", type=int, default=64, help="Slice size in MB (default: 64)")
    p_regen.add_argument("--workers", type=int, default=None, help="Parallel workers (default: CPU count - 1)")
    p_regen.add_argument("--label", default="mistral_tensors", help="Tensor label for RNG context")
    p_regen.add_argument("--allow-procedural", action="store_true",
                         help="REQUIRED: Acknowledge that this produces noise, not recovered weights")
    p_regen.set_defaults(func=cmd_regen_procedural)

    # fidelity-create-residual command (FIDELITY = bit-exact reconstruction)
    p_fidelity_create = subparsers.add_parser(
        "fidelity-create-residual",
        help="Create XOR residual for fidelity (bit-exact) reconstruction",
        description="""
FIDELITY Regeneration: Create XOR residual between baseline and procedural GGUF.

This stores the difference so you can later reconstruct the exact original:
  original = procedural ^ residual

The XOR residual is currently 1:1 sized. Future versions may compress it.

See docs/REGEN_TAXONOMY.md for the distinction between procedural and fidelity regen.

Example:
  python -m helix_cdc fidelity-create-residual \\
    --baseline-gguf /path/to/vendor.gguf \\
    --regen-gguf /path/to/procedural.gguf \\
    --manifest /path/to/manifest.json \\
    --residual-out residuals/instance.bin
        """
    )
    p_fidelity_create.add_argument("--baseline-gguf", required=True, help="Path to baseline (original) GGUF")
    p_fidelity_create.add_argument("--regen-gguf", required=True, help="Path to procedural GGUF")
    p_fidelity_create.add_argument("--manifest", required=True, help="Path to capsule manifest JSON (will be updated)")
    p_fidelity_create.add_argument("--residual-out", required=True, help="Relative path for residual output")
    p_fidelity_create.set_defaults(func=cmd_fidelity_create_residual)

    # fidelity-reconstruct command (FIDELITY = bit-exact reconstruction)
    p_fidelity_recon = subparsers.add_parser(
        "fidelity-reconstruct",
        help="Bit-exact reconstruction from baseline + residual",
        description="""
FIDELITY Reconstruction: Reconstruct exact original from baseline + XOR residual.

  output = baseline ^ residual

Output SHA256 WILL match the original file (verified fidelity).

See docs/REGEN_TAXONOMY.md for the distinction between procedural and fidelity regen.

Example:
  python -m helix_cdc fidelity-reconstruct \\
    --baseline-gguf /path/to/vendor.gguf \\
    --residual /path/to/residual.bin \\
    --output /path/to/reconstructed.gguf
        """
    )
    p_fidelity_recon.add_argument("--baseline-gguf", required=True, help="Path to baseline GGUF")
    p_fidelity_recon.add_argument("--residual", required=True, help="Path to residual bytes")
    p_fidelity_recon.add_argument("--output", required=True, help="Path for reconstructed GGUF")
    p_fidelity_recon.set_defaults(func=cmd_fidelity_reconstruct)

    # DEPRECATED: regen-gguf-template - DISPROVEN (0 percent template match rate)
    # Kept for backwards compatibility but warns user
    p_template = subparsers.add_parser(
        "regen-gguf-template",
        help="[DEPRECATED] Template-based reconstruction - DISPROVEN (0 percent match rate)",
        description="""
*** DEPRECATED - THIS METHOD IS DISPROVEN ***

Testing on 2026-01-05 showed that the template library has 0.0002 percent match rate
against real GGUF data (1 block out of 550,580).

See docs/REGEN_TAXONOMY.md for proven alternatives:
  - fidelity-reconstruct (requires residual from original)
  - CAS regeneration (chunk_vault_hardened.py)

This command is kept for backwards compatibility but will likely fail.
        """
    )
    p_template.add_argument("--dna", required=True, help="DNA seed hex")
    p_template.add_argument("--entropy", required=True, help="Entropy seed hex")
    p_template.add_argument("--header-source", required=True, help="Reference GGUF for header")
    p_template.add_argument("--output", required=True, help="Output path")
    p_template.add_argument("--template-lib", help="Path to template library")
    p_template.set_defaults(func=cmd_regen_gguf_template)

    # fidelity-from-manifest command (FIDELITY = bit-exact from manifest)
    p_fidelity_manifest = subparsers.add_parser(
        "fidelity-from-manifest",
        help="Bit-exact reconstruction using manifest instance_residual",
        description="""
FIDELITY Reconstruction: Reconstruct exact original using baseline + manifest residual.

Reads the manifest JSON, locates the residual file, verifies hashes, and performs:
  output = baseline ^ residual

Output SHA256 WILL match the original (verified fidelity).

See docs/REGEN_TAXONOMY.md for the distinction between procedural and fidelity regen.

Example:
  python -m helix_cdc fidelity-from-manifest \\
    --baseline-gguf /path/to/vendor.gguf \\
    --manifest /path/to/capsule/manifest.json \\
    --output /path/to/reconstructed.gguf
        """
    )
    p_fidelity_manifest.add_argument("--baseline-gguf", required=True, help="Path to baseline GGUF")
    p_fidelity_manifest.add_argument("--manifest", required=True, help="Path to capsule manifest JSON with instance_residual")
    p_fidelity_manifest.add_argument("--output", required=True, help="Path for reconstructed GGUF")
    p_fidelity_manifest.set_defaults(func=cmd_fidelity_from_manifest)

    # ============================================
    # WO#50: Crypto-DNA Commands
    # ============================================

    # dna-inspect command
    p_dna_inspect = subparsers.add_parser(
        "dna-inspect",
        help="Inspect Crypto-DNA structure from a Base64 seed",
    )
    p_dna_inspect.add_argument("--seed", required=True, help="Base64-encoded DNA seed")
    p_dna_inspect.add_argument("--json", action="store_true", help="Output JSON summary")
    p_dna_inspect.set_defaults(func=cmd_dna_inspect)

    # dna-grow command
    p_dna_grow = subparsers.add_parser(
        "dna-grow",
        help="Grow phenotype from Crypto-DNA seed",
    )
    p_dna_grow.add_argument("--seed", required=True, help="Base64-encoded DNA seed")
    p_dna_grow.add_argument("--output", required=True, help="Output file path")
    p_dna_grow.add_argument("--shape", default="1024", help="Output shape (comma-separated, e.g. '1024' or '32,32')")
    p_dna_grow.add_argument("--dtype", default="float32", help="Output dtype (float32, float16, uint8)")
    p_dna_grow.add_argument("--format", default="npy", choices=["npy", "raw", "json"], help="Output format")
    p_dna_grow.set_defaults(func=cmd_dna_grow)

    # dna-bind command
    p_dna_bind = subparsers.add_parser(
        "dna-bind",
        help="Bind Crypto-DNA to a capsule manifest",
    )
    p_dna_bind.add_argument("--seed", required=True, help="Base64-encoded DNA seed")
    p_dna_bind.add_argument("--manifest", required=True, help="Path to capsule manifest JSON")
    p_dna_bind.set_defaults(func=cmd_dna_bind)

    # dna-to-digest command
    p_dna_digest = subparsers.add_parser(
        "dna-to-digest",
        help="Convert Crypto-DNA seed to digest (hash summary)",
    )
    p_dna_digest.add_argument("--seed", required=True, help="Base64-encoded DNA seed")
    p_dna_digest.set_defaults(func=cmd_dna_to_digest)

    # digest-to-dna command
    p_digest_dna = subparsers.add_parser(
        "digest-to-dna",
        help="Look up DNA seed from digest (requires manifest)",
    )
    p_digest_dna.add_argument("--digest", required=True, help="SHA256 digest to look up")
    p_digest_dna.add_argument("--manifest", required=True, help="Path to capsule manifest JSON")
    p_digest_dna.set_defaults(func=cmd_digest_to_dna)

    # ============================================
    # Echo Self-Awareness Commands
    # ============================================

    # receipts-index command
    p_receipts_index = subparsers.add_parser(
        "receipts-index",
        help="Scan receipts directory and emit index of all receipts",
        description="""
Echo Self-Awareness: Scan the receipts/ directory and emit a JSON index
containing all receipts with their paths, timestamps, claims, and status.

This allows Echo to "see herself" - what proofs exist and what their status is.

Example:
  python -m helix_cdc receipts-index
  python -m helix_cdc receipts-index --output receipts/INDEX.json
        """
    )
    p_receipts_index.add_argument(
        "--receipts-dir",
        default="receipts",
        help="Path to receipts directory (default: receipts)"
    )
    p_receipts_index.add_argument(
        "--output",
        help="Output file path (default: stdout)"
    )
    p_receipts_index.set_defaults(func=cmd_receipts_index)

    # self-explain command
    p_self_explain = subparsers.add_parser(
        "self-explain",
        help="Generate self-explanation report of Echo capabilities",
        description="""
Echo Self-Awareness: Generate a report explaining what Echo can do,
what methods are proven vs disproven, and key facts about the system.

This loads the tool_registry.yaml and produces a JSON summary.

Example:
  python -m helix_cdc self-explain
  python -m helix_cdc self-explain --output echo_capabilities.json
        """
    )
    p_self_explain.add_argument(
        "--output",
        help="Output file path (default: stdout)"
    )
    p_self_explain.set_defaults(func=cmd_self_explain)

    # ============================================
    # QUBO Batch Planner Command
    # ============================================

    p_qubo_planner = subparsers.add_parser(
        "qubo-planner",
        help="Batch job scheduling via QUBO optimization",
        description="""
QUBO Batch Planner: Multi-job backend assignment using QUBO optimization.

This is where QPU infrastructure becomes meaningful (64+ jobs = 192+ vars).
Uses brute force for small batches (<=8 jobs) and simulated annealing for larger.

Example:
  python -m helix_cdc qubo-planner --n-jobs 16
  python -m helix_cdc qubo-planner --n-jobs 64 --sweeps 2000
  python -m helix_cdc qubo-planner --jobs-file jobs.json

Solver auto-selection:
  - <=8 jobs (<=24 vars): Brute force enumeration
  - >8 jobs (>24 vars): Simulated Annealing
  - >64 jobs (future): D-Wave QPU if authenticated
        """
    )
    p_qubo_planner.add_argument(
        "--n-jobs", "-n",
        type=int,
        default=16,
        help="Number of synthetic jobs to generate (default: 16)"
    )
    p_qubo_planner.add_argument(
        "--jobs-file",
        help="Path to JSON file with job definitions (overrides --n-jobs)"
    )
    p_qubo_planner.add_argument(
        "--seed",
        type=int,
        default=42,
        help="Random seed for synthetic jobs and SA (default: 42)"
    )
    p_qubo_planner.add_argument(
        "--sweeps",
        type=int,
        default=1000,
        help="SA sweeps for large problems (default: 1000)"
    )
    p_qubo_planner.add_argument(
        "--num-reads",
        type=int,
        default=10,
        help="Number of SA samples (default: 10)"
    )
    p_qubo_planner.add_argument(
        "--brute-force-max",
        type=int,
        default=8,
        help="Max jobs for brute force solver (default: 8)"
    )
    p_qubo_planner.add_argument(
        "--gpu-vram",
        type=float,
        default=4000,
        help="GPU VRAM in MB (default: 4000)"
    )
    p_qubo_planner.add_argument(
        "--qpu",
        action="store_true",
        help="Enable QPU availability (simulated)"
    )
    p_qubo_planner.add_argument(
        "--receipt-dir",
        help="Directory for receipt output (default: receipts/planner)"
    )
    p_qubo_planner.add_argument(
        "--output", "-o",
        help="Output full results to JSON file"
    )
    p_qubo_planner.add_argument(
        "--quiet", "-q",
        action="store_true",
        help="Suppress detailed assignment output"
    )
    p_qubo_planner.set_defaults(func=cmd_qubo_planner)

    # ============================================
    # CDNAv2 Commands
    # ============================================

    # cdna-convert-v2 command
    p_cdna_convert = subparsers.add_parser(
        "cdna-convert-v2",
        help="Convert CDNA v1 files to v2 format",
        description="""
Convert CDNA v1 (.cdna.hxz) files to v2 (.cdna2.hxz) format.

CDNAv2 adds:
  - Per-block brotli compression (32 rows default)
  - Block index for random access
  - Per-block SHA256 verification

Examples:
  # Convert single file
  python -m helix_cdc cdna-convert-v2 --input tensor.cdna.hxz

  # Convert directory with parallel workers
  python -m helix_cdc cdna-convert-v2 \\
      --input seeds/cdna_shards_f16/ \\
      --output seeds/cdna_v2/ \\
      --parallel 8 --verify
        """
    )
    p_cdna_convert.add_argument(
        "--input", "-i",
        required=True,
        help="Input v1 file or directory"
    )
    p_cdna_convert.add_argument(
        "--output", "-o",
        help="Output v2 file or directory (default: auto-named)"
    )
    p_cdna_convert.add_argument(
        "--block-rows",
        type=int,
        default=32,
        help="Rows per block (default: 32)"
    )
    p_cdna_convert.add_argument(
        "--parallel", "-j",
        type=int,
        default=1,
        help="Parallel workers for batch conversion (default: 1)"
    )
    p_cdna_convert.add_argument(
        "--verify",
        action="store_true",
        help="Verify conversion (compare indices/codebook)"
    )
    p_cdna_convert.add_argument(
        "--pattern",
        default="*.hxz",
        help="Glob pattern for batch input (default: *.hxz)"
    )
    p_cdna_convert.add_argument(
        "--receipt",
        help="Receipt output path (default: receipts/cdna_v2_conversion.json)"
    )
    p_cdna_convert.set_defaults(func=cmd_cdna_convert_v2)

    # cdna-verify command
    p_cdna_verify = subparsers.add_parser(
        "cdna-verify",
        help="Verify all blocks in a CDNAv2 file",
        description="""
Verify SHA256 checksums for all blocks in a CDNAv2 file.

This decompresses each block and checks its SHA256 against the stored value.
Use to detect corruption or tampering.

Example:
  python -m helix_cdc cdna-verify seeds/cdna_v2/blk.0.attn_k.weight.cdna2.hxz
        """
    )
    p_cdna_verify.add_argument(
        "file",
        help="CDNAv2 file to verify"
    )
    p_cdna_verify.set_defaults(func=cmd_cdna_verify)

    # cdna-info command
    p_cdna_info = subparsers.add_parser(
        "cdna-info",
        help="Show CDNA file metadata",
        description="""
Display metadata for CDNA v1 or v2 files.

Shows tensor name, shape, codebook stats, block information (v2 only).

Example:
  python -m helix_cdc cdna-info tensor.cdna2.hxz
  python -m helix_cdc cdna-info --json tensor.cdna2.hxz
        """
    )
    p_cdna_info.add_argument(
        "file",
        help="CDNA file to inspect"
    )
    p_cdna_info.add_argument(
        "--json",
        action="store_true",
        help="Output as JSON"
    )
    p_cdna_info.add_argument(
        "--blocks",
        action="store_true",
        help="Show block index details (v2 only)"
    )
    p_cdna_info.set_defaults(func=cmd_cdna_info)

    # ============================================
    # CDNA Kernel + Agent Commands (WO-CDNA-KERNEL-V1)
    # ============================================

    # cdna-fetch-rows command
    p_cdna_fetch = subparsers.add_parser(
        "cdna-fetch-rows",
        help="Fetch rows from CDNAv2 file using kernel primitive",
        description="""
Fetch a range of rows from a CDNAv2 file using the kernel primitive.

The kernel provides:
  - address(row_range) -> block tuples
  - fetch_block(block_id) -> decompressed indices
  - verify_block(block_id) -> SHA256 check

Example:
  python -m helix_cdc cdna-fetch-rows tensor.cdna2.hxz --start 100 --end 200 --verify
        """
    )
    p_cdna_fetch.add_argument("file", help="CDNAv2 file")
    p_cdna_fetch.add_argument("--start", type=int, default=0, help="Start row (default: 0)")
    p_cdna_fetch.add_argument("--end", type=int, help="End row (default: all)")
    p_cdna_fetch.add_argument("--verify", action="store_true", help="Verify blocks via SHA256")
    p_cdna_fetch.add_argument("--output", "-o", help="Output path for rows (.npy)")
    p_cdna_fetch.add_argument("--json", action="store_true", help="Output receipt as JSON")
    p_cdna_fetch.set_defaults(func=cmd_cdna_fetch_rows)

    # cdna-matmul command
    p_cdna_matmul = subparsers.add_parser(
        "cdna-matmul",
        help="Compute y = W @ x using CDNA kernel streaming",
        description="""
Compute matrix-vector product y = W @ x where W is CDNAv2-compressed.

Memory-efficient: W is never materialized as dense float32.
Uses CDNA dot product trick: bincount + codebook matmul.

Example:
  python -m helix_cdc cdna-matmul tensor.cdna2.hxz --input x.npy --output y.npy --verify
        """
    )
    p_cdna_matmul.add_argument("file", help="CDNAv2 file (W matrix)")
    p_cdna_matmul.add_argument("--input", "-i", help="Input vector path (.npy) or random if not specified")
    p_cdna_matmul.add_argument("--output", "-o", help="Output vector path (.npy)")
    p_cdna_matmul.add_argument("--verify", action="store_true", help="Verify blocks via SHA256")
    p_cdna_matmul.add_argument("--seed", type=int, default=42, help="Random seed for input (if not specified)")
    p_cdna_matmul.add_argument("--json", action="store_true", help="Output receipt as JSON")
    p_cdna_matmul.set_defaults(func=cmd_cdna_matmul)

    # cdna-cache-stats command
    p_cdna_cache = subparsers.add_parser(
        "cdna-cache-stats",
        help="Show agent cache statistics",
        description="""
Display FibPi agent cache statistics after performing access operations.

Tests different verify policies (always, sampled, trust_cached) and shows
cache hit rates, verification counts, and memory usage.

Example:
  python -m helix_cdc cdna-cache-stats tensor.cdna2.hxz --accesses 50
        """
    )
    p_cdna_cache.add_argument("file", help="CDNAv2 file to test")
    p_cdna_cache.add_argument("--accesses", type=int, default=20, help="Number of random accesses (default: 20)")
    p_cdna_cache.add_argument("--policy", choices=["always", "sampled", "trust_cached"], default="trust_cached")
    p_cdna_cache.add_argument("--json", action="store_true", help="Output as JSON")
    p_cdna_cache.set_defaults(func=cmd_cdna_cache_stats)

    # cdna-kernel-kat command (Known Answer Test)
    p_cdna_kat = subparsers.add_parser(
        "cdna-kernel-kat",
        help="Run kernel Known Answer Test (deterministic correctness check)",
        description="""
Run a Known Answer Test (KAT) for the CDNA kernel.

Uses a fixed seed to generate a deterministic input vector, computes y = W @ x,
and emits a receipt with the output SHA256 for reproducibility verification.

Example:
  python -m helix_cdc cdna-kernel-kat tensor.cdna2.hxz
  python -m helix_cdc cdna-kernel-kat tensor.cdna2.hxz --receipt receipts/kernel_kat.json
        """
    )
    p_cdna_kat.add_argument("file", help="CDNAv2 file")
    p_cdna_kat.add_argument("--seed", type=int, default=1337, help="RNG seed for input (default: 1337)")
    p_cdna_kat.add_argument("--receipt", help="Receipt output path")
    p_cdna_kat.set_defaults(func=cmd_cdna_kernel_kat)

    # cdna-benchmark command
    p_cdna_bench = subparsers.add_parser(
        "cdna-benchmark",
        help="Benchmark CDNA compression/decompression",
        description="""
Benchmark CDNAv2 file operations:
  - Full tensor read
  - Random row access
  - Streaming matmul
  - Block verification

Example:
  python -m helix_cdc cdna-benchmark tensor.cdna2.hxz
  python -m helix_cdc cdna-benchmark --iterations 10 tensor.cdna2.hxz
        """
    )
    p_cdna_bench.add_argument(
        "file",
        help="CDNAv2 file to benchmark"
    )
    p_cdna_bench.add_argument(
        "--iterations", "-n",
        type=int,
        default=3,
        help="Number of iterations (default: 3)"
    )
    p_cdna_bench.set_defaults(func=cmd_cdna_benchmark)

    # ============================================
    # FGIP Causality Engine Commands
    # ============================================

    # fgip-init-db command
    p_fgip_init = subparsers.add_parser(
        "fgip-init-db",
        help="Initialize FGIP causality database",
        description="""
Initialize the FGIP Causality Engine SQLite database.

Creates all required tables for nodes, edges, sources, and claims.
Optionally loads seed data from a directory.

Example:
  python -m helix_cdc fgip-init-db --db data/fgip/fgip.db
  python -m helix_cdc fgip-init-db --db data/fgip/fgip.db --seed-dir seeds/fgip/
        """
    )
    p_fgip_init.add_argument(
        "--db",
        default="data/fgip/fgip.db",
        help="Path to SQLite database (default: data/fgip/fgip.db)"
    )
    p_fgip_init.add_argument(
        "--sources-dir",
        default="data/fgip/sources",
        help="Directory for source artifacts (default: data/fgip/sources)"
    )
    p_fgip_init.add_argument(
        "--seed-dir",
        help="Optional directory containing seed_nodes.json and seed_edges.json"
    )
    p_fgip_init.set_defaults(func=cmd_fgip_init_db)

    # fgip-load-seed command
    p_fgip_load = subparsers.add_parser(
        "fgip-load-seed",
        help="Load seed data into FGIP database",
        description="""
Load nodes and edges from JSON seed files.

Example:
  python -m helix_cdc fgip-load-seed --db data/fgip/fgip.db \\
      --nodes seeds/fgip/seed_nodes.json \\
      --edges seeds/fgip/seed_edges.json
        """
    )
    p_fgip_load.add_argument(
        "--db",
        default="data/fgip/fgip.db",
        help="Path to SQLite database"
    )
    p_fgip_load.add_argument(
        "--nodes",
        required=True,
        help="Path to seed_nodes.json"
    )
    p_fgip_load.add_argument(
        "--edges",
        required=True,
        help="Path to seed_edges.json"
    )
    p_fgip_load.add_argument(
        "--sources-dir",
        default="data/fgip/sources",
        help="Directory for source artifacts"
    )
    p_fgip_load.set_defaults(func=cmd_fgip_load_seed)

    # fgip-add-node command
    p_fgip_add_node = subparsers.add_parser(
        "fgip-add-node",
        help="Add a node to the FGIP graph",
        description="""
Add a node (entity) to the causality graph.

Example:
  python -m helix_cdc fgip-add-node --db data/fgip/fgip.db \\
      --id "ORG:acme_corp" --type ORGANIZATION --name "Acme Corporation"
        """
    )
    p_fgip_add_node.add_argument(
        "--db",
        default="data/fgip/fgip.db",
        help="Path to SQLite database"
    )
    p_fgip_add_node.add_argument(
        "--id",
        required=True,
        help="Node ID (e.g., 'ORG:acme_corp')"
    )
    p_fgip_add_node.add_argument(
        "--type",
        required=True,
        help="Node type (e.g., ORGANIZATION, PERSON, LEGISLATION)"
    )
    p_fgip_add_node.add_argument(
        "--name",
        required=True,
        help="Human-readable name"
    )
    p_fgip_add_node.set_defaults(func=cmd_fgip_add_node)

    # fgip-add-edge command
    p_fgip_add_edge = subparsers.add_parser(
        "fgip-add-edge",
        help="Add an edge to the FGIP graph",
        description="""
Add an edge (relationship) to the causality graph.

Without --source-url or --source-file, edge status is CLAIMED.
With a source, status is promoted to EVIDENCED.

Example:
  python -m helix_cdc fgip-add-edge --db data/fgip/fgip.db \\
      --type LOBBIED --from "ORG:chamber" --to "LEG:bill_123"

  python -m helix_cdc fgip-add-edge --db data/fgip/fgip.db \\
      --type FUNDED --from "ORG:donor" --to "ORG:recipient" \\
      --source-url "https://example.com/report.pdf"
        """
    )
    p_fgip_add_edge.add_argument(
        "--db",
        default="data/fgip/fgip.db",
        help="Path to SQLite database"
    )
    p_fgip_add_edge.add_argument(
        "--type",
        required=True,
        help="Edge type (e.g., LOBBIED, FUNDED, OWNS_SHARES)"
    )
    p_fgip_add_edge.add_argument(
        "--from",
        dest="from_node",
        required=True,
        help="Source node ID"
    )
    p_fgip_add_edge.add_argument(
        "--to",
        dest="to_node",
        required=True,
        help="Target node ID"
    )
    p_fgip_add_edge.add_argument(
        "--source-url",
        help="URL to fetch as evidence source"
    )
    p_fgip_add_edge.add_argument(
        "--source-file",
        help="Local file path as evidence source"
    )
    p_fgip_add_edge.add_argument(
        "--sensitivity",
        choices=["LOW", "MED", "HIGH"],
        default="LOW",
        help="Sensitivity level (default: LOW)"
    )
    p_fgip_add_edge.add_argument(
        "--assertion-level",
        choices=["FACT", "INFERENCE", "HYPOTHESIS"],
        default="INFERENCE",
        help="Assertion level: FACT (proven), INFERENCE (plausible), HYPOTHESIS (private/spicy)"
    )
    p_fgip_add_edge.add_argument(
        "--visibility",
        choices=["PRIVATE", "PUBLIC"],
        default="PRIVATE",
        help="Visibility: PRIVATE (internal), PUBLIC (can be exported)"
    )
    p_fgip_add_edge.add_argument(
        "--sources-dir",
        default="data/fgip/sources",
        help="Directory for source artifacts"
    )
    p_fgip_add_edge.set_defaults(func=cmd_fgip_add_edge)

    # fgip-attach-source command
    p_fgip_attach = subparsers.add_parser(
        "fgip-attach-source",
        help="Attach a source to an existing edge",
        description="""
Attach a source artifact to an edge, promoting it to EVIDENCED status.

Example:
  python -m helix_cdc fgip-attach-source --db data/fgip/fgip.db \\
      edge_abc123 --url "https://example.com/doc.pdf"

  python -m helix_cdc fgip-attach-source --db data/fgip/fgip.db \\
      edge_abc123 --file local/evidence.pdf
        """
    )
    p_fgip_attach.add_argument(
        "edge_id",
        help="Edge ID to attach source to"
    )
    p_fgip_attach.add_argument(
        "--db",
        default="data/fgip/fgip.db",
        help="Path to SQLite database"
    )
    p_fgip_attach.add_argument(
        "--url",
        help="URL to fetch as source"
    )
    p_fgip_attach.add_argument(
        "--file",
        help="Local file path as source"
    )
    p_fgip_attach.add_argument(
        "--excerpt",
        help="Claim excerpt from source"
    )
    p_fgip_attach.add_argument(
        "--page",
        help="Page reference"
    )
    p_fgip_attach.add_argument(
        "--sources-dir",
        default="data/fgip/sources",
        help="Directory for source artifacts"
    )
    p_fgip_attach.set_defaults(func=cmd_fgip_attach_source)

    # fgip-path command
    p_fgip_path = subparsers.add_parser(
        "fgip-path",
        help="Trace causal paths between nodes",
        description="""
Find and display causal paths between two nodes with source coverage scores.

Example:
  python -m helix_cdc fgip-path --db data/fgip/fgip.db \\
      --from "ORG:us_chamber" --to "LEG:pntr_2000"
        """
    )
    p_fgip_path.add_argument(
        "--db",
        default="data/fgip/fgip.db",
        help="Path to SQLite database"
    )
    p_fgip_path.add_argument(
        "--from",
        dest="from_node",
        required=True,
        help="Starting node ID"
    )
    p_fgip_path.add_argument(
        "--to",
        dest="to_node",
        required=True,
        help="Target node ID"
    )
    p_fgip_path.add_argument(
        "--max-depth",
        type=int,
        default=10,
        help="Maximum path length (default: 10)"
    )
    p_fgip_path.add_argument(
        "--json",
        action="store_true",
        help="Output as JSON"
    )
    p_fgip_path.set_defaults(func=cmd_fgip_path)

    # fgip-ownership-loop command
    p_fgip_loop = subparsers.add_parser(
        "fgip-ownership-loop",
        help="Detect ownership cycles",
        description="""
Detect ownership cycles starting from an entity.

Example:
  python -m helix_cdc fgip-ownership-loop --db data/fgip/fgip.db "ORG:holding_co"
        """
    )
    p_fgip_loop.add_argument(
        "node",
        help="Starting node ID"
    )
    p_fgip_loop.add_argument(
        "--db",
        default="data/fgip/fgip.db",
        help="Path to SQLite database"
    )
    p_fgip_loop.add_argument(
        "--json",
        action="store_true",
        help="Output as JSON"
    )
    p_fgip_loop.set_defaults(func=cmd_fgip_ownership_loop)

    # fgip-contradictions command
    p_fgip_contra = subparsers.add_parser(
        "fgip-contradictions",
        help="Check for contradictions",
        description="""
Run contradiction checks on the graph.

Example:
  python -m helix_cdc fgip-contradictions --db data/fgip/fgip.db
  python -m helix_cdc fgip-contradictions --db data/fgip/fgip.db --entity "ORG:acme"
        """
    )
    p_fgip_contra.add_argument(
        "--db",
        default="data/fgip/fgip.db",
        help="Path to SQLite database"
    )
    p_fgip_contra.add_argument(
        "--entity",
        help="Check specific entity only"
    )
    p_fgip_contra.add_argument(
        "--json",
        action="store_true",
        help="Output as JSON"
    )
    p_fgip_contra.set_defaults(func=cmd_fgip_contradictions)

    # fgip-correction-score command
    p_fgip_score = subparsers.add_parser(
        "fgip-correction-score",
        help="Compute correction score for an entity",
        description="""
Compute an auditable correction score for an entity.

The score reflects how well-sourced and verified the entity's relationships are.

Example:
  python -m helix_cdc fgip-correction-score --db data/fgip/fgip.db "ORG:example_corp"
        """
    )
    p_fgip_score.add_argument(
        "entity",
        help="Node ID to score"
    )
    p_fgip_score.add_argument(
        "--db",
        default="data/fgip/fgip.db",
        help="Path to SQLite database"
    )
    p_fgip_score.add_argument(
        "--json",
        action="store_true",
        help="Output as JSON"
    )
    p_fgip_score.set_defaults(func=cmd_fgip_correction_score)

    # fgip-export command
    p_fgip_export = subparsers.add_parser(
        "fgip-export",
        help="Export the FGIP graph",
        description="""
Export the graph to JSON format.

PUBLIC MODE (--public):
- HYPOTHESIS edges are NEVER exported
- Only FACT + INFERENCE with EVIDENCED status
- Only edges marked visibility=PUBLIC
- Includes disclaimer

Example:
  python -m helix_cdc fgip-export --db data/fgip/fgip.db \\
      --format json --output /tmp/fgip_export.json

  # Public-safe export:
  python -m helix_cdc fgip-export --db data/fgip/fgip.db \\
      --public --output /tmp/fgip_public.json
        """
    )
    p_fgip_export.add_argument(
        "--db",
        default="data/fgip/fgip.db",
        help="Path to SQLite database"
    )
    p_fgip_export.add_argument(
        "--format",
        choices=["json", "cytoscape"],
        default="json",
        help="Export format (default: json)"
    )
    p_fgip_export.add_argument(
        "--output", "-o",
        required=True,
        help="Output file path"
    )
    p_fgip_export.add_argument(
        "--max-sensitivity",
        choices=["LOW", "MED", "HIGH"],
        default="LOW",
        help="Maximum sensitivity level to include (default: LOW)"
    )
    p_fgip_export.add_argument(
        "--public",
        action="store_true",
        help="Public mode: only export FACT+INFERENCE with EVIDENCED status, never HYPOTHESIS"
    )
    p_fgip_export.set_defaults(func=cmd_fgip_export)

    # fgip-stats command
    p_fgip_stats = subparsers.add_parser(
        "fgip-stats",
        help="Show FGIP database statistics",
        description="""
Display statistics about the FGIP database.

Example:
  python -m helix_cdc fgip-stats --db data/fgip/fgip.db
        """
    )
    p_fgip_stats.add_argument(
        "--db",
        default="data/fgip/fgip.db",
        help="Path to SQLite database"
    )
    p_fgip_stats.add_argument(
        "--json",
        action="store_true",
        help="Output as JSON"
    )
    p_fgip_stats.set_defaults(func=cmd_fgip_stats)

    # fgip-lint command
    p_fgip_lint = subparsers.add_parser(
        "fgip-lint",
        help="Lint text for lawsuit-inviting phrases",
        description="""
Detect "catnip" phrases that invite lawsuits and suggest safe replacements.

CATNIP PHRASES (avoid these):
- controls, coordinating, corrupt, captured, illegal
- colluding, conspiracy, fraud, criminal, bribed, rigged

SAFE ALTERNATIVES:
- "has measurable influence via"
- "structure creates aligned incentives"
- "funding model creates conflicts"
- "pattern is consistent with coordinated incentives"

Example:
  python -m helix_cdc fgip-lint "The company controls the regulator"
  python -m helix_cdc fgip-lint --file notes.txt
        """
    )
    p_fgip_lint.add_argument(
        "text",
        nargs="?",
        help="Text to lint (or use --file)"
    )
    p_fgip_lint.add_argument(
        "--file", "-f",
        help="File containing text to lint"
    )
    p_fgip_lint.add_argument(
        "--fix",
        action="store_true",
        help="Output sanitized version with replacements"
    )
    p_fgip_lint.set_defaults(func=cmd_fgip_lint)

    # fgip-narrative command
    p_fgip_narrative = subparsers.add_parser(
        "fgip-narrative",
        help="Generate forensic narrative for an edge",
        description="""
Generate a "polite f*** you" style narrative for an edge relationship.

TONES:
- forensic: "According to [source]...", labeled assertions (default)
- neutral: Plain language, still safe
- aggressive: Private only, never public

Example:
  python -m helix_cdc fgip-narrative --db data/fgip/fgip.db edge_abc123
  python -m helix_cdc fgip-narrative --db data/fgip/fgip.db edge_abc123 --tone neutral
        """
    )
    p_fgip_narrative.add_argument(
        "edge_id",
        help="Edge ID to generate narrative for"
    )
    p_fgip_narrative.add_argument(
        "--db",
        default="data/fgip/fgip.db",
        help="Path to SQLite database"
    )
    p_fgip_narrative.add_argument(
        "--tone",
        choices=["forensic", "neutral", "aggressive"],
        default="forensic",
        help="Output tone (default: forensic)"
    )
    p_fgip_narrative.add_argument(
        "--sources-dir",
        default="data/fgip/sources",
        help="Directory for source artifacts"
    )
    p_fgip_narrative.set_defaults(func=cmd_fgip_narrative)

    args = parser.parse_args()

    if not args.command:
        parser.print_help()
        return 1

    # Handle workers default
    if hasattr(args, 'workers') and args.workers is None:
        from multiprocessing import cpu_count
        args.workers = max(1, cpu_count() - 1)

    return args.func(args)


if __name__ == "__main__":
    sys.exit(main())
