#!/usr/bin/env python3
"""
helix_infer_glyph.py
--------------------

Seed-Only Inference Engine (Scaffold)
=====================================

This is the *correct* compressed-domain inference scaffold:

- NO tensor regeneration here.
- NO GGUF / safetensors.
- Operates entirely on:
    - capsule seeds (dna_pattern, dimensions, vault_coordinates)
    - a "vault" object (shared structure)
    - glyph-level state machines

IMPORTANT:
    This file DOES NOT pretend to implement a trained language model.

Instead, it gives you a clean, deterministic, testable skeleton that:

    1. Parses a real .hxz capsule
    2. Loads (or placeholders) a vault object
    3. Encodes prompts into glyph streams
    4. Runs a symbolic "inference loop" over glyphs + vault
    5. Produces repeatable outputs
    6. NEVER regenerates numeric tensors

All learned behavior must be plugged into the TODO sections.

You can run it as a CLI:

    python3 helix_cdc/helix_infer_glyph.py \
        --capsule /home/voidstr3m33/archive/fused_capsules/arctic.hxz \
        --vault /path/to/vault.bin \
        --prompt "hello, world" \
        --max-new-tokens 32

This will:
    - Print a JSON summary (capsule metadata, glyph trace, output tokens)
    - Print metrics (vault size, steps, duration)
"""

from __future__ import annotations

import argparse
import dataclasses
import hashlib
import json
import sys
import time
import zlib
from pathlib import Path
from typing import List, Dict, Any, Optional, Tuple, TYPE_CHECKING
import struct

import numpy as np


# ---------------------------------------------------------------------------
# Lexical projection vocabulary (256 words = 64 base × 4 semantic lanes)
# ---------------------------------------------------------------------------

# Import position-aware 256-word vocabulary from vocab_256 module.
# Structure: 64 base indices × 4 semantic lanes (pos_mod4)
#   Lane 0: ENTITY (nouns/objects)
#   Lane 1: ACTION (verbs)
#   Lane 2: QUALITY (adjectives/modifiers)
#   Lane 3: RELATION (prepositions/conjunctions)
#
# This integrates with MorphSAT position-based adjacency from flowtorch.

from helix_cdc.vocab_256 import (
    vocab_lookup,
    attention_to_words,
    b64_char_to_idx,
    VOCAB_256,
    VOCAB_ENTITY,
    VOCAB_ACTION,
    VOCAB_QUALITY,
    VOCAB_RELATION,
)

# MorphSAT integration (WO#31)
from helix_cdc.morphsat import MorphSAT, score_token_sequence

# Keep VOCAB_64 for backwards compatibility (first 64 words = ENTITY lane)
VOCAB_64 = VOCAB_ENTITY


# ---------------------------------------------------------------------------
# Capsule representation (.hxz)
# ---------------------------------------------------------------------------


@dataclasses.dataclass
class HelixCapsule:
    path: Path
    entropy: float
    sha256_hex: str
    dna_pattern: str
    dimensions: int
    compression_ratio: float
    vault_coordinates: List[float]
    meta: Dict[str, Any]

    @classmethod
    def load(cls, path: Path) -> "HelixCapsule":
        """
        Parse .hxz capsule. Handles two formats:

        Format A (JSON payload):
            #HXZ_START
            # entropy: 7.154973
            # sha256: ad31550...
            #====
            <zlib-compressed JSON with dna_pattern, dimensions, vault_coordinates, ...>

        Format B (raw payload, metadata in header):
            #HXZ_START
            # schema_version: proof_v1
            # entropy: 1.835211
            # sha256: e5b844...
            # original_size: 10485760
            # compression_ratio: 31022.96
            #====
            <zlib-compressed raw bytes>
        """
        raw = path.read_bytes()
        header, payload = cls._split_header_payload(raw)

        # Parse header comments
        entropy = 0.0
        sha256_hex = ""
        dimensions = 0
        compression_ratio = 0.0
        original_size = 0
        dna_pattern = ""
        header_meta: Dict[str, str] = {}

        for line in header.splitlines():
            line = line.strip()
            if line.startswith(b"# ") and b":" in line:
                key_val = line[2:].split(b":", 1)
                if len(key_val) == 2:
                    key = key_val[0].strip().decode("ascii", "ignore")
                    val = key_val[1].strip().decode("utf-8", "ignore")
                    header_meta[key] = val

                    if key == "entropy":
                        try:
                            entropy = float(val)
                        except ValueError:
                            pass
                    elif key == "sha256":
                        sha256_hex = val
                    elif key == "dimensions":
                        try:
                            dimensions = int(val)
                        except ValueError:
                            pass
                    elif key == "compression_ratio":
                        try:
                            compression_ratio = float(val)
                        except ValueError:
                            pass
                    elif key == "original_size":
                        try:
                            original_size = int(val)
                        except ValueError:
                            pass
                    elif key in ("dna_pattern", "dna"):
                        dna_pattern = val

        # Try to decompress and parse as JSON
        meta: Dict[str, Any] = {}
        vault_coordinates: List[float] = []

        # Check for HelixZ DNA capsule format (no payload, DNA in header body)
        if not payload and header.startswith(b"# HelixZ capsule:"):
            # Extract DNA pattern from body
            lines = header.decode("utf-8", "ignore").splitlines()
            dna_lines = []
            for line in lines:
                line = line.strip()
                if line and not line.startswith("#"):
                    # This is DNA pattern data (GATTACA...)
                    if all(c in "ACGTNX" for c in line):
                        dna_lines.append(line)
            dna_pattern = "".join(dna_lines) if dna_lines else ""
            if not dna_pattern:
                # Use hash of header as fallback
                dna_pattern = hashlib.sha256(header).hexdigest()[:64]
            meta = {
                "format": "helixz_dna",
                "header": header_meta,
                "dna_length": len(dna_pattern),
            }
            # Estimate dimensions from DNA length
            if dimensions == 0:
                dimensions = max(8, min(72, len(dna_pattern) // 100 + 8))
        elif not payload:
            # No payload and not HelixZ - use header hash as DNA
            if not dna_pattern:
                dna_pattern = hashlib.sha256(header).hexdigest()[:64]
            meta = {
                "format": "header_only",
                "header": header_meta,
            }
            if dimensions == 0:
                dimensions = max(8, min(72, int(entropy * 10) if entropy else 16))
        elif payload[:6] == b'\xfd7zXZ\x00':
            # xz compressed
            try:
                import lzma
                decompressed = lzma.decompress(payload)
                if not dna_pattern:
                    dna_pattern = hashlib.sha256(decompressed[:1024]).hexdigest()[:64]
                meta = {
                    "format": "xz_compressed",
                    "payload_size": len(decompressed),
                    "header": header_meta,
                }
                if dimensions == 0:
                    dimensions = max(8, min(72, int(entropy * 10) if entropy else 32))
            except Exception as e:
                if not dna_pattern:
                    dna_pattern = hashlib.sha256(payload[:1024]).hexdigest()[:64]
                meta = {
                    "format": "xz_error",
                    "error": str(e),
                    "header": header_meta,
                }
                if dimensions == 0:
                    dimensions = 16
        else:
            # Standard zlib payload
            try:
                decompressed = zlib.decompress(payload)
                # Try JSON first
                try:
                    meta = json.loads(decompressed.decode("utf-8"))
                    dna_pattern = meta.get("dna_pattern") or meta.get("dna") or dna_pattern
                    dimensions = int(meta.get("dimensions", dimensions))
                    compression_ratio = float(meta.get("compression_ratio", compression_ratio))
                    vault_coordinates = meta.get("vault_coordinates") or meta.get("fibpi_coords") or []
                except (json.JSONDecodeError, UnicodeDecodeError):
                    # Not JSON - it's raw compressed data
                    if not dna_pattern:
                        dna_pattern = hashlib.sha256(decompressed[:1024]).hexdigest()[:64]
                    meta = {
                        "format": "raw_payload",
                        "payload_size": len(decompressed),
                        "header": header_meta,
                    }
                    if dimensions == 0:
                        dimensions = max(8, min(72, int(entropy * 10) if entropy else 16))
            except zlib.error as e:
                # Payload might not be zlib compressed
                if not dna_pattern:
                    dna_pattern = hashlib.sha256(payload[:1024]).hexdigest()[:64]
                meta = {
                    "format": "raw_uncompressed",
                    "payload_size": len(payload),
                    "header": header_meta,
                    "decompress_error": str(e),
                }
                if dimensions == 0:
                    dimensions = max(8, min(72, int(entropy * 10) if entropy else 16))

        return cls(
            path=path,
            entropy=entropy,
            sha256_hex=sha256_hex,
            dna_pattern=dna_pattern,
            dimensions=dimensions,
            compression_ratio=compression_ratio,
            vault_coordinates=vault_coordinates,
            meta=meta,
        )

    @staticmethod
    def _split_header_payload(raw: bytes) -> Tuple[bytes, bytes]:
        """
        Split .hxz file into header and payload.

        Supports multiple formats:

        Format A (HXZ_START + #====):
            #HXZ_START
            # key: value
            ...
            #====
            <zlib compressed payload>

        Format B (HelixZ capsule + DNA):
            # HelixZ capsule: model_name.gguf
            # Compressed @ timestamp
            GATTACAGATTACA...
            <no payload - DNA IS the data>

        Format C (raw xz):
            <xz magic bytes: 0xFD 0x37 0x7A 0x58 0x5A>
            <xz compressed data>
        """
        # Check for xz magic (0xFD, '7', 'z', 'X', 'Z')
        if raw[:6] == b'\xfd7zXZ\x00':
            # Raw xz compressed - no header
            return b"", raw

        # Check for HelixZ capsule format (DNA-based)
        if raw.startswith(b"# HelixZ capsule:"):
            # This format uses DNA pattern directly
            return raw, b""

        # Check for HXZ_START format
        marker = b"#====\n"
        idx = raw.find(marker)
        if idx == -1:
            # Try alternate format without trailing newline
            marker = b"#===="
            idx = raw.find(marker)
            if idx == -1:
                # No standard delimiter found - treat entire file as header
                # (might be a pure DNA capsule or different format)
                return raw, b""
            header = raw[:idx]
            payload = raw[idx + len(marker):]
            # Skip any leading whitespace/newlines before zlib magic
            payload = payload.lstrip(b'\n\r')
        else:
            header = raw[:idx]
            payload = raw[idx + len(marker):]

        return header, payload


# ---------------------------------------------------------------------------
# Vault representation (shared structure)
# ---------------------------------------------------------------------------

_VAULT_MAGIC = b"HXV1"
_DTYPE_CODE_FLOAT32 = 1


@dataclasses.dataclass
class HelixVault:
    """
    Vault wrapper with structured format support.

    Now understands a structured vault format written by
    tools/build_vault_from_npy.py:

        [0:4]   magic:    b'HXV1'
        [4:8]   uint32:   num_basis (K)
        [8:12]  uint32:   dim
        [12:13] uint8:    dtype_code (1 = float32)
        [13:16] padding
        [16:...] float32  basis data, row-major (K x dim)

    If the file doesn't match this format, we fall back to opaque bytes
    and still behave deterministically (but with less structure).
    """

    path: Optional[Path]
    data: bytes
    vault_id: str

    # Lazy cache for parsed basis matrix
    _basis: Optional[np.ndarray] = dataclasses.field(default=None, init=False, repr=False)

    @classmethod
    def load(cls, path: Optional[Path], capsule: "HelixCapsule") -> "HelixVault":
        """
        Load vault from path if given; otherwise derive a tiny in-memory vault
        from capsule metadata.

        IMPORTANT:
            If no path is provided, this vault is a PLACEHOLDER. It is
            deterministic, but NOT trained. Real systems should point
            --vault to a learned manifold file (e.g., .vault from realistic_weights).
        """
        if path is not None:
            p = path.expanduser().resolve()
            data = p.read_bytes()
            vault_id = cls._hash_bytes(data)[:16]
            return cls(path=p, data=data, vault_id=vault_id)

        # Placeholder: derive some stable bytes from capsule metadata.
        h = hashlib.sha256()
        h.update(capsule.dna_pattern.encode("utf-8"))
        h.update(str(capsule.dimensions).encode("ascii"))
        h.update(",".join(map(str, capsule.vault_coordinates)).encode("utf-8"))
        data = h.digest()
        vault_id = h.hexdigest()[:16]
        return cls(path=None, data=data, vault_id=vault_id)

    @staticmethod
    def _hash_bytes(b: bytes) -> str:
        return hashlib.sha256(b).hexdigest()

    # ------------------------------------------------------------------
    # Structured view: basis matrix
    # ------------------------------------------------------------------
    def basis_matrix(self) -> Optional[np.ndarray]:
        """
        Return vault basis as a NumPy array [num_basis, dim] if the underlying
        data is in HXV1 format. Otherwise, return None.

        Basis is kept cached in _basis for efficiency.
        """
        if self._basis is not None:
            return self._basis

        if len(self.data) < 16:
            return None

        header = self.data[:16]
        try:
            magic, num_basis, dim, dtype_code = struct.unpack("<4sIIB3x", header)
        except struct.error:
            return None

        if magic != _VAULT_MAGIC:
            return None
        if dtype_code != _DTYPE_CODE_FLOAT32:
            return None

        expected_bytes = 16 + num_basis * dim * 4
        if len(self.data) != expected_bytes:
            # Size mismatch - treat as opaque
            return None

        payload = self.data[16:]
        mat = np.frombuffer(payload, dtype=np.float32)
        mat = mat.reshape(num_basis, dim)
        self._basis = mat
        return mat


# ---------------------------------------------------------------------------
# Glyph representation and engine
# ---------------------------------------------------------------------------


@dataclasses.dataclass
class GlyphState:
    """
    A tiny glyph-level state container.

    This keeps:
      - current glyph sequence
      - a rolling hash (for determinism / tracing)
      - an opaque, model-specific "latent" vector (just bytes here)
      - basis_idx_history: which vault basis indices were selected at each step
    """

    glyphs: List[str]
    rolling_hash_hex: str
    latent: bytes
    basis_idx_history: List[int] = dataclasses.field(default_factory=list)


class HelixGlyphInference:
    """
    Seed-only inference engine scaffold.

    This engine:
      - NEVER regenerates numeric tensors
      - Works entirely with:
          capsule.dna_pattern
          vault.data
          glyph sequences
      - Provides deterministic, stateful stepping over glyphs

    !!! CRITICAL !!!
        This class DOES NOT encode any real LLM semantics yet.
        All "model behavior" must be implemented in:
            - _encode_prompt_to_glyphs()
            - _glyph_step()
            - _glyphs_to_tokens()

        Right now, those are deterministic placeholders.
        They are intentionally honest about being scaffolding,
        not magic.
    """

    def __init__(self, capsule: HelixCapsule, vault: HelixVault, debug: bool = False):
        self.capsule = capsule
        self.vault = vault
        self.debug = debug

        # Derive a stable engine ID from capsule + vault
        h = hashlib.sha256()
        h.update(capsule.dna_pattern.encode("utf-8"))
        h.update(vault.vault_id.encode("ascii"))
        self.engine_id = h.hexdigest()[:16]

    # -------------------------------
    # Public API
    # -------------------------------

    def infer(self, prompt: str, max_new_tokens: int = 32) -> Dict[str, Any]:
        """
        Run seed-only "inference" for a prompt. Returns a dict with:
            - prompt
            - glyph_trace
            - output_tokens (placeholder ids/strings)
            - metadata (engine_id, capsule path, etc.)
        """
        start = time.time()

        init_state = self._encode_prompt_to_glyphs(prompt)
        if self.debug:
            print(f"[DEBUG] Initial glyphs: {init_state.glyphs}", file=sys.stderr)

        glyph_trace: List[Dict[str, Any]] = []
        state = init_state

        for step in range(max_new_tokens):
            state = self._glyph_step(state, step)
            glyph_trace.append(
                {
                    "step": step,
                    "glyphs": "".join(state.glyphs),
                    "rolling_hash": state.rolling_hash_hex,
                }
            )

        output_tokens, cog_frame = self._glyphs_to_tokens(state)

        # Apply MorphSAT scoring to tokens (WO#31)
        morphsat_scores = self._score_with_morphsat(output_tokens, cog_frame)

        duration = time.time() - start

        return {
            "prompt": prompt,
            "output_tokens": output_tokens,
            "cog_frame": cog_frame,
            "morphsat_scores": morphsat_scores,
            "glyph_trace": glyph_trace,
            "metadata": {
                "engine_id": self.engine_id,
                "capsule": str(self.capsule.path),
                "vault_id": self.vault.vault_id,
                "capsule_dimensions": self.capsule.dimensions,
                "capsule_entropy": self.capsule.entropy,
                "vault_size_bytes": len(self.vault.data),
                "tensors_regenerated": 0,
                "duration_s": duration,
                "cog_frame_len": len(cog_frame),
                "morphsat_penalized_count": sum(1 for s in morphsat_scores if s.get("penalized")),
            },
        }

    # -------------------------------
    # Internal helpers (PLACEHOLDERS)
    # -------------------------------

    def _score_with_morphsat(
        self, tokens: List[str], cog_frame: List[Dict[str, Any]]
    ) -> List[Dict[str, Any]]:
        """
        Score token sequence using MorphSAT adjacency constraints (WO#31).

        Args:
            tokens: List of output token words
            cog_frame: List of cog_frame dicts with base_idx info

        Returns:
            List of score dicts for each token
        """
        if not tokens or not cog_frame:
            return []

        # Extract base indices from cog_frame
        indices = [frame.get("base_idx", 0) for frame in cog_frame]

        # Score using MorphSAT
        return score_token_sequence(tokens, indices)

    def _encode_prompt_to_glyphs(self, prompt: str) -> GlyphState:
        """
        Deterministically encode the prompt into a glyph sequence.

        CURRENT IMPLEMENTATION:
            - Uses a simple mapping: take chars from dna_pattern and mix with prompt
            - Rolling hash seeds the latent

        This is NOT a real embedding. It's a placeholder that MUST be replaced
        with a trained mapping from tokens -> glyphs in the future.
        """
        dna = self.capsule.dna_pattern or "ATGC"
        if not dna:
            dna = "ATGC"

        glyphs: List[str] = []
        # Interleave prompt bytes with dna_pattern characters
        for i, ch in enumerate(prompt.encode("utf-8")):
            dna_ch = dna[i % len(dna)]
            # Map byte to a small alphabet (for now, DNA + a few extras)
            glyph = chr(97 + (ch % 26))  # 'a'..'z'
            glyphs.append(dna_ch)
            glyphs.append(glyph)

        # Compute initial latent from capsule + prompt
        h = hashlib.sha256()
        h.update(self.capsule.dna_pattern.encode("utf-8"))
        h.update(prompt.encode("utf-8"))
        h.update(self.vault.data)
        latent = h.digest()
        rolling_hash_hex = h.hexdigest()

        return GlyphState(glyphs=glyphs, rolling_hash_hex=rolling_hash_hex, latent=latent)

    # -------------------------------------------------------------------------
    # Helix Compressed-Domain Attention Operator
    # -------------------------------------------------------------------------

    def _attention_step(
        self, state: GlyphState, basis: np.ndarray, temperature: float = 0.5
    ) -> Tuple[int, np.ndarray]:
        """
        Compressed-domain attention step.

        Inputs:
            - state: current GlyphState (glyph history, latent)
            - basis: vault basis matrix [K, D] from SVD
            - temperature: softmax temperature (lower = sharper, higher = more uniform)

        Outputs:
            - chosen_basis_idx: int (index 0..K-1)
            - attended_vector: np.ndarray shape [D]

        This approximates:
            Q = f(glyph_state)
            K = basis vectors
            V = basis vectors
            attn = softmax(QK^T / (sqrt(D) * temp))
            output = attn @ V

        Everything is done in compressed space.
        Still deterministic, still zero tensor regeneration.
        """
        K, D = basis.shape

        # --------------------------------------------------------------
        # 1. Build Query vector Q from glyph history (deterministic)
        # --------------------------------------------------------------
        h = hashlib.sha256()
        for g in state.glyphs:
            h.update(g.encode("utf-8"))
        h.update(state.latent)

        # Q_raw = 32 bytes → upcast to float32 vector of dimension D
        q_bytes = h.digest()
        q_vec = np.frombuffer(q_bytes, dtype=np.uint8).astype(np.float32)
        q_vec = np.resize(q_vec, (D,))  # expand to match dim
        q_vec = q_vec / (np.linalg.norm(q_vec) + 1e-8)

        # --------------------------------------------------------------
        # 2. Compute attention logits: QK^T / (sqrt(D) * temperature)
        # --------------------------------------------------------------
        scale = np.sqrt(D) * max(temperature, 0.01)
        logits = (basis @ q_vec) / scale  # shape [K]

        # --------------------------------------------------------------
        # 3. softmax over logits
        # --------------------------------------------------------------
        logits = logits - logits.max()
        weights = np.exp(logits)
        weights = weights / (weights.sum() + 1e-8)  # shape [K]

        # --------------------------------------------------------------
        # 4. Expected value of V under weights
        # --------------------------------------------------------------
        attended = weights @ basis  # shape [D]

        # normalize for stability
        attended = attended / (np.linalg.norm(attended) + 1e-8)

        # --------------------------------------------------------------
        # 5. Pick top basis index (deterministic) for lexical projection
        #    Use weighted sampling seed for more variety while staying deterministic
        # --------------------------------------------------------------
        # Deterministic selection based on weighted sum of indices
        weighted_idx = np.sum(np.arange(K) * weights)
        # Add hash-based offset for variety
        offset = int.from_bytes(state.latent[:4], "little") % K
        top_idx = int((weighted_idx + offset) % K)

        return top_idx, attended

    def _glyph_step(self, state: GlyphState, step_idx: int) -> GlyphState:
        """
        Single symbolic "step" in glyph space.

        ATTENTION-BASED IMPLEMENTATION:

            - If vault has a structured basis matrix (K x dim), we:
                1. Run compressed-domain attention: Q·K^T / sqrt(D) → softmax
                2. Pick top-attended basis index
                3. Fold attended vector bytes into latent hash
                4. Record the basis_idx for lexical projection

            - If no structured basis is available, we fall back to the
              pure-hash behavior.

        This is still deterministic and NOT a full LLM, but:
            - Uses real attention geometry over SVD manifold
            - basis_idx_history enables mapping to vocabulary words
            - Zero tensor regeneration
        """
        basis = self.vault.basis_matrix()

        h = hashlib.sha256()
        h.update("".join(state.glyphs).encode("utf-8"))
        h.update(self.vault.data)
        h.update(str(step_idx).encode("ascii"))

        basis_idx = -1  # -1 means no structured basis

        if basis is not None:
            # Use attention to select basis vector
            chosen_idx, attn_vec = self._attention_step(state, basis)

            # Fold attended vector into hash (first 64 bytes)
            h.update(attn_vec.tobytes()[:64])

            basis_idx = chosen_idx

        new_latent = h.digest()
        rolling_hash_hex = h.hexdigest()

        # Derive a new glyph from hash output
        idx = new_latent[0] % 26
        new_glyph = chr(97 + idx)  # 'a'..'z'

        new_glyphs = state.glyphs + [new_glyph]
        new_basis_history = state.basis_idx_history + [basis_idx]

        if self.debug:
            print(
                f"[DEBUG] step={step_idx} new_glyph={new_glyph} basis_idx={basis_idx} "
                f"rolling_hash={rolling_hash_hex} attention_used={basis is not None}",
                file=sys.stderr,
            )

        return GlyphState(
            glyphs=new_glyphs,
            rolling_hash_hex=rolling_hash_hex,
            latent=new_latent,
            basis_idx_history=new_basis_history,
        )

    def _glyphs_to_tokens(self, state: GlyphState) -> Tuple[List[str], List[Dict[str, Any]]]:
        """
        Map final glyph state into an output "token" sequence with cog_frame metadata.

        UPDATED IMPLEMENTATION (256-word position-aware vocabulary):
            - If we have basis_idx_history (from structured vault), map each
              basis index to a vocabulary word using position-aware lookup:
                  Position 0: ENTITY (nouns)
                  Position 1: ACTION (verbs)
                  Position 2: QUALITY (adjectives)
                  Position 3: RELATION (prepositions/connectors)
            - This creates more natural sentence-like structure.
            - If no structured vault was used (all indices are -1), fall back
              to the old hash-based placeholder tokens.

        Returns:
            Tuple of (words, cog_frame) where cog_frame is a list of dicts:
                {
                    "pos": int,           # position in sequence
                    "lane": int,          # 0-3 semantic lane
                    "lane_name": str,     # ENTITY/ACTION/QUALITY/RELATION
                    "base_idx": int,      # 0-63 base vocabulary index
                    "word": str,          # resolved word
                    "source": str,        # "vault_basis" or "dna_b64"
                }

        This is LEXICAL PROJECTION: vault basis vectors + position → vocabulary words.
        Still deterministic, still zero tensor regen, but now produces
        actual English words with grammatical structure.
        """
        LANE_NAMES = ["ENTITY", "ACTION", "QUALITY", "RELATION"]

        # Check if we have structured basis history
        has_structured_basis = any(idx >= 0 for idx in state.basis_idx_history)

        if has_structured_basis:
            # Lexical projection: map basis indices to words using position-aware vocab
            words: List[str] = []
            cog_frame: List[Dict[str, Any]] = []
            pos = 0  # track position for semantic lane selection

            for idx in state.basis_idx_history:
                if idx >= 0:
                    # Use position-aware lookup: vocab_lookup(base_idx, pos_mod4)
                    lane = pos % 4
                    word = vocab_lookup(idx, lane)
                    words.append(word)
                    cog_frame.append({
                        "pos": pos,
                        "lane": lane,
                        "lane_name": LANE_NAMES[lane],
                        "base_idx": idx,
                        "word": word,
                        "source": "vault_basis",
                    })
                    pos += 1
                # Skip indices that are -1 (no basis used for that step)

            return words, cog_frame

        # Fallback: DNA-to-vocab projection using base64 tokenizer (WO#27)
        # Convert DNA pattern (hex) to bytes, then base64, then vocab_256 words
        import base64

        dna = self.capsule.dna_pattern or ""
        glyph_str = "".join(state.glyphs)

        # Combine DNA pattern + glyphs for deterministic seed
        combined = (dna + glyph_str).encode("utf-8")

        # Convert to base64
        b64_str = base64.b64encode(combined).decode("ascii")

        # Map base64 characters to vocab_256 words using position-aware lookup
        words: List[str] = []
        cog_frame: List[Dict[str, Any]] = []
        for pos, char in enumerate(b64_str[:32]):  # Limit to 32 tokens
            idx = b64_char_to_idx(char)  # 0-63
            lane = pos % 4
            word = vocab_lookup(idx, lane)  # position-aware semantic lane
            words.append(word)
            cog_frame.append({
                "pos": pos,
                "lane": lane,
                "lane_name": LANE_NAMES[lane],
                "base_idx": idx,
                "b64_char": char,
                "word": word,
                "source": "dna_b64",
            })

        return words, cog_frame


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------


def parse_args(argv: Optional[List[str]] = None) -> argparse.Namespace:
    ap = argparse.ArgumentParser(
        description="HELIX Seed-Only Inference (Glyph/Vault Scaffold)"
    )
    ap.add_argument(
        "--capsule",
        required=True,
        help="Path to .hxz capsule (e.g. arctic.hxz)",
    )
    ap.add_argument(
        "--vault",
        help="Optional path to a learned vault file. If omitted, a small placeholder vault is derived from the capsule.",
    )
    ap.add_argument(
        "--prompt",
        required=True,
        help="Input prompt text.",
    )
    ap.add_argument(
        "--max-new-tokens",
        type=int,
        default=32,
        help="Number of glyph steps to run.",
    )
    ap.add_argument(
        "--json",
        action="store_true",
        help="Print full JSON output instead of human summary.",
    )
    ap.add_argument(
        "--debug",
        action="store_true",
        help="Print debug info to stderr.",
    )
    return ap.parse_args(argv)


def main(argv: Optional[List[str]] = None) -> None:
    args = parse_args(argv)

    capsule_path = Path(args.capsule).expanduser().resolve()
    if not capsule_path.is_file():
        print(f"ERROR: Capsule not found: {capsule_path}", file=sys.stderr)
        sys.exit(1)

    vault_path: Optional[Path] = None
    if args.vault:
        vp = Path(args.vault).expanduser().resolve()
        if not vp.is_file():
            print(f"ERROR: Vault file not found: {vp}", file=sys.stderr)
            sys.exit(1)
        vault_path = vp

    capsule = HelixCapsule.load(capsule_path)
    vault = HelixVault.load(vault_path, capsule)

    engine = HelixGlyphInference(capsule=capsule, vault=vault, debug=args.debug)
    result = engine.infer(prompt=args.prompt, max_new_tokens=args.max_new_tokens)

    if args.json:
        print(json.dumps(result, indent=2, sort_keys=True))
        return

    # Human-readable summary
    meta = result["metadata"]
    print("=" * 70)
    print("HELIX SEED-ONLY INFERENCE (Glyph/Vault Scaffold)")
    print("=" * 70)
    print(f"Capsule      : {meta['capsule']}")
    print(f"Engine ID    : {meta['engine_id']}")
    print(f"Vault ID     : {meta['vault_id']}")
    print(f"Vault size   : {meta['vault_size_bytes']} bytes")
    print(f"Dimensions   : {meta['capsule_dimensions']}")
    print(f"Entropy      : {meta['capsule_entropy']}")
    print(f"Tensors regen: {meta['tensors_regenerated']}")
    print(f"Duration     : {meta['duration_s']:.4f} s")
    print("-" * 70)
    print(f"Prompt       : {result['prompt']!r}")
    print("-" * 70)
    print("Output tokens (symbolic, placeholder):")
    print("  " + " ".join(result["output_tokens"]))
    print("-" * 70)
    print("NOTE: This is a deterministic scaffold. To become a real model,")
    print("      replace the TODO sections with trained vault + operators.")
    print("=" * 70)


if __name__ == "__main__":
    main()
