//! AXON Parser — recursive descent, fail-fast.
//!
//! Direct port of axon/compiler/parser.py.
//!
//! Tier 1 constructs (persona, context, anchor, memory, tool, type,
//! flow, step, intent, run, epistemic, if, for, let, return) are
//! fully parsed into typed AST nodes.
//!
//! Tier 2+ constructs are parsed structurally (balanced braces) into
//! `GenericDeclaration` / `GenericFlowStep`.

use crate::ast::*;
use crate::tokens::{is_declaration_keyword, Token, TokenType, Trivia, TriviaKind};

// Comment token kinds the lexer now emits (Fase 14.a). The parser
// filters these out of its working stream — they are materialised into
// a parallel `Trivia` array indexed by effective-token position, then
// attached to `Program.declaration_trivia[i]` once each declaration's
// span is known.
const fn is_comment_token(tt: &TokenType) -> bool {
    matches!(
        tt,
        TokenType::LineComment
            | TokenType::BlockComment
            | TokenType::DocLineComment
            | TokenType::DocBlockComment
            | TokenType::InnerDocLineComment
            | TokenType::InnerDocBlockComment
    )
}

const fn token_to_trivia_kind(tt: &TokenType) -> Option<TriviaKind> {
    match tt {
        TokenType::LineComment => Some(TriviaKind::Line),
        TokenType::BlockComment => Some(TriviaKind::Block),
        TokenType::DocLineComment => Some(TriviaKind::DocLine),
        TokenType::DocBlockComment => Some(TriviaKind::DocBlock),
        TokenType::InnerDocLineComment => Some(TriviaKind::InnerDocLine),
        TokenType::InnerDocBlockComment => Some(TriviaKind::InnerDocBlock),
        _ => None,
    }
}

/// Fase 14.b — write `leading_trivia` and `trailing_trivia` into the
/// per-struct fields of a `Declaration` variant.
///
/// Mirrors what the Python parser does automatically via its
/// `_with_trivia` decorator on every `_parse_*` method. In Rust we
/// do it once at the top of the parse loop so the spread to every
/// variant is in a single place.
fn attach_trivia_to_decl(decl: &mut Declaration, leading: Vec<Trivia>, trailing: Vec<Trivia>) {
    match decl {
        Declaration::Import(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Persona(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Context(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Anchor(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Memory(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Tool(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Type(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Flow(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Intent(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Run(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Epistemic(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Let(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::LambdaData(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Agent(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Shield(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Pix(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Psyche(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Corpus(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Dataspace(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Ots(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Mandate(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Compute(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Daemon(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Extension(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::AxonStore(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::AxonEndpoint(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Resource(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Fabric(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Manifest(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Observe(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Reconcile(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Lease(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Ensemble(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Session(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Topology(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Immune(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Reflex(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Heal(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Component(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::View(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Channel(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Socket(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
        Declaration::Generic(n) => {
            n.leading_trivia = leading;
            n.trailing_trivia = trailing;
        }
    }
}

// ── Public error type ────────────────────────────────────────────────────────

/// §Fase 28.d — Source-context constants. D4 ratified 2026-05-10:
/// 2 lines before + 2 lines after the error line. Mirror of the
/// Python-side `_SOURCE_CONTEXT_LINES_BEFORE` / `_AFTER` so the
/// rustc-style block has identical shape across stacks.
pub const SOURCE_CONTEXT_LINES_BEFORE: usize = 2;
pub const SOURCE_CONTEXT_LINES_AFTER: usize = 2;

/// §Fase 28.d — Rustc-style source-context block for a parse error.
///
/// Holds a reference to the source text plus the line/column the
/// error points at. Rendering is lazy — call ``render()`` to format
/// the block (line numbers + caret + 2 lines before + 2 after).
///
/// Pure and deterministic: no ANSI colors, no terminal-width
/// detection. Output shape is byte-identical to the Python
/// `SourceSnippet.render()` on the same input — that's the cross-
/// stack drift gate (28.i).
#[derive(Debug, Clone)]
pub struct SourceSnippet {
    pub source: String,
    pub line: u32,
    pub column: u32,
    pub filename: String,
    pub context_before: usize,
    pub context_after: usize,
}

impl SourceSnippet {
    /// Construct with the default 2/2 context window.
    pub fn new(source: String, line: u32, column: u32, filename: String) -> Self {
        Self {
            source,
            line,
            column,
            filename,
            context_before: SOURCE_CONTEXT_LINES_BEFORE,
            context_after: SOURCE_CONTEXT_LINES_AFTER,
        }
    }

    /// Format the snippet as a multi-line rustc-style block.
    ///
    /// Empty source → empty string. Out-of-range line → empty
    /// string. Caret column is clamped to `[1, line_len + 1]`.
    /// Output shape matches Python `SourceSnippet.render` byte-
    /// identically per D7.
    #[must_use]
    pub fn render(&self) -> String {
        if self.source.is_empty() || self.line < 1 {
            return String::new();
        }
        let raw: Vec<&str> = self.source.split('\n').collect();
        // Match Python's str.splitlines() trailing-newline shape:
        // strip an empty trailing entry produced by a final '\n'.
        let lines: Vec<&str> = if raw.last() == Some(&"") {
            raw[..raw.len() - 1].to_vec()
        } else {
            raw
        };
        if lines.is_empty() || self.line as usize > lines.len() {
            return String::new();
        }

        let line_idx = self.line as usize;
        let start = line_idx.saturating_sub(self.context_before).max(1);
        let end = (line_idx + self.context_after).min(lines.len());

        let gutter = end.to_string().len();
        let empty_gutter = " ".repeat(gutter);

        let mut out: Vec<String> = Vec::with_capacity(end - start + 4);
        out.push(format!(
            "{empty_gutter} --> {}:{}:{}",
            self.filename, self.line, self.column
        ));
        out.push(format!("{empty_gutter} |"));
        for n in start..=end {
            let line_text = lines[n - 1];
            out.push(format!("{n:>gutter$} | {line_text}", gutter = gutter));
            if n == line_idx {
                let line_len = line_text.chars().count();
                let col = (self.column as usize).clamp(1, line_len + 1);
                out.push(format!(
                    "{empty_gutter} | {pad}^",
                    pad = " ".repeat(col - 1)
                ));
            }
        }
        out.join("\n")
    }
}

#[derive(Debug, Clone, Default)]
pub struct ParseError {
    pub message: String,
    pub line: u32,
    pub column: u32,
    /// §Fase 28.d — Optional rustc-style source-context block.
    /// `None` preserves the legacy single-line shape; populated by
    /// `Parser::with_source` callers (and by `parse_with_recovery`
    /// / `parse` when a source has been attached to the parser).
    /// Existing struct-literal call sites use the `..Default::default()`
    /// idiom (default = None) to stay terse.
    pub source_snippet: Option<SourceSnippet>,
}

impl ParseError {
    /// §Fase 28.d — Attach a `SourceSnippet` derived from raw source
    /// text and filename. Returns `self` so the call can be chained
    /// at the construction site. No-op when `line == 0`. Idempotent.
    #[must_use]
    pub fn attach_source(mut self, source: &str, filename: &str) -> Self {
        if self.line >= 1 {
            self.source_snippet = Some(SourceSnippet::new(
                source.to_string(),
                self.line,
                self.column,
                filename.to_string(),
            ));
        }
        self
    }
}

impl std::fmt::Display for ParseError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "[line {}:{}] {}", self.line, self.column, self.message)?;
        if let Some(snippet) = &self.source_snippet {
            let block = snippet.render();
            if !block.is_empty() {
                write!(f, "\n{block}")?;
            }
        }
        Ok(())
    }
}

impl std::error::Error for ParseError {}

// ── §Fase 28.c — Public recovery result ──────────────────────────────────────
//
// Mirror of Python's `axon.compiler.parser.ParseResult` (Fase 28.b).
// The rationale, sync semantics, and test contract are documented in
// `docs/fase/fase_28_adopter_diagnostic_robustness.md`. The Rust frontend
// must produce structurally identical error lists to the Python parser
// when handed the same source — that is the cross-stack drift gate
// (D7 ratified 2026-05-10: byte-identical error lists).
//
// `program` holds whatever declarations the parser was able to parse
// successfully. `errors` holds every recovered error in source order.
// A clean parse returns `errors.is_empty()`; the existing fail-fast
// `parse()` API is preserved verbatim per D9.

/// Outcome of `Parser::parse_with_recovery` — partial program plus the
/// list of every error the parser recovered from. See module docs for
/// the panic-mode + sync-point recovery semantics.
#[derive(Debug)]
pub struct ParseResult {
    pub program: Program,
    pub errors: Vec<ParseError>,
}

impl ParseResult {
    /// True iff at least one parse error was recovered. Callers that
    /// want to short-circuit on failure should check this rather than
    /// relying on `program.declarations.is_empty()` (the parser may
    /// have salvaged some declarations even with errors present).
    #[inline]
    #[must_use]
    pub fn has_errors(&self) -> bool {
        !self.errors.is_empty()
    }

    /// Inverse of `has_errors`. Convenience for the "happy path" check
    /// in tests + adopter integrations.
    #[inline]
    #[must_use]
    pub fn is_clean(&self) -> bool {
        self.errors.is_empty()
    }
}

/// §Fase 28.c — Top-level declaration keywords used as resync points
/// during error recovery (D2 ratified 2026-05-10). Mirrors the
/// `_TOP_LEVEL_DECLARATION_KEYWORDS` frozenset on the Python side.
///
/// Distinct from `tokens::is_declaration_keyword` because that helper
/// is used by the structural declaration counter and intentionally
/// excludes some grammar-only tokens (Know/Believe/Speculate/Doubt,
/// Ingest, Ots) that DO begin a top-level declaration in
/// `parse_declaration` and therefore must be valid sync points.
///
/// Adding a new top-level dispatch arm in `parse_declaration` MUST
/// add the corresponding token here so the recovery walker can
/// re-sync correctly.
#[inline]
const fn is_top_level_decl_kw_for_recovery(tt: &TokenType) -> bool {
    matches!(
        tt,
        TokenType::Import
            | TokenType::Persona
            | TokenType::Context
            | TokenType::Anchor
            | TokenType::Memory
            | TokenType::Tool
            | TokenType::Type
            | TokenType::Flow
            | TokenType::Intent
            | TokenType::Run
            | TokenType::Let
            | TokenType::Know
            | TokenType::Believe
            | TokenType::Speculate
            | TokenType::Doubt
            | TokenType::Lambda
            | TokenType::Agent
            | TokenType::Shield
            | TokenType::Pix
            | TokenType::Psyche
            | TokenType::Corpus
            | TokenType::Dataspace
            | TokenType::Ots
            | TokenType::Mandate
            | TokenType::Compute
            | TokenType::Daemon
            | TokenType::AxonStore
            | TokenType::AxonEndpoint
            | TokenType::Resource
            | TokenType::Fabric
            | TokenType::Manifest
            | TokenType::Observe
            | TokenType::Reconcile
            | TokenType::Lease
            | TokenType::Ensemble
            | TokenType::Session
            | TokenType::Topology
            | TokenType::Immune
            | TokenType::Reflex
            | TokenType::Heal
            | TokenType::Component
            | TokenType::View
            | TokenType::Channel
            | TokenType::Ingest
            | TokenType::Persist
            | TokenType::Retrieve
            | TokenType::Mutate
            | TokenType::Purge
            | TokenType::Transact
            | TokenType::Mcp
    )
}

// ── §Fase 30.b — axonendpoint transport + keepalive closed enums ────────────
//
// D2 ratified 2026-05-10: `transport` is a closed enum
// {json, sse, ndjson}. D6 ratified: `keepalive` is a closed enum
// {5s, 15s, 30s, 60s}. Both mirror the Python frontend's
// `_AXONENDPOINT_TRANSPORT_VALUES` / `_AXONENDPOINT_KEEPALIVE_VALUES`
// frozensets in `axon/compiler/parser.py`. Cross-stack drift gate
// (30.b fixture) asserts byte-identical parse for every entry.

/// Adopter-facing acceptable values for `transport:` field.
/// Used by both the parser (validation + smart-suggest) and the
/// type-checker (30.c) so adopter tooling sees one canonical list.
pub const AXONENDPOINT_TRANSPORT_VALUES: &[&str] = &["json", "sse", "ndjson"];

/// §Fase 33.z.k.b (v1.28.0) — Closed-catalog SSE wire-format
/// dialects. Selected via the parametrized grammar
/// `transport: sse(<dialect>)`; bare `transport: sse` resolves to
/// the Q1 default per the flow's algebraic-effect predicate
/// (openai for tool-streaming flows; axon for type-annotation-only).
///
/// Vertical-grounded scope (Q3 revised 2026-05-14): five dialects
/// cover ~99% of LLM-streaming adopter expectations.
///   - `axon`      — current W3C named events
///                   (event: axon.token / event: axon.complete).
///                   D6 backwards-compat baseline; indefinitely
///                   supported as a first-class option.
///   - `openai`    — `data: {"choices":[{"delta":{...}}]}` frames
///                   terminated by `data: [DONE]`. OpenAI Chat
///                   Completions streaming wire verbatim.
///   - `kimi`      — Moonshot Kimi (kimi.moonshot.cn) — uses the
///                   OpenAI-compatible Chat Completions wire format
///                   verbatim (same chunk shape, same `data: [DONE]`
///                   sentinel). First-class entry so adopters
///                   declare intent explicitly; under the hood the
///                   wire is identical to `openai`.
///   - `glm`       — Zhipu ChatGLM (open.bigmodel.cn) — same as
///                   kimi, uses OpenAI-compat wire. First-class
///                   entry for adopter clarity.
///   - `anthropic` — `event: content_block_delta` frames terminated
///                   by `event: message_stop`. Adopter SDKs
///                   targeting Anthropic Claude consume this shape
///                   verbatim.
///
/// Why kimi + glm as first-class entries (Q3 revision rationale):
/// Bemarking AI's primary adopter pipelines through Kimi K2.x +
/// Zhipu GLM-4.x. While the wire IS byte-identical to OpenAI's
/// Chat Completions streaming, declaring `transport: sse(kimi)` /
/// `transport: sse(glm)` lets the audit trail + observability
/// surfaces correlate adopter intent against the underlying
/// provider — without the adopter having to know that "kimi
/// happens to be OpenAI-compat on the wire today". The runtime
/// dispatches kimi + glm to the same `OpenAIDialectAdapter` so
/// the wire shape stays canonical-OpenAI-bytes.
///
/// Open-set adapter pluggability (downstream crates registering
/// custom dialects) remains explicitly out of scope per the
/// Axon-for-Axon discipline.
pub const AXONENDPOINT_TRANSPORT_DIALECTS: &[&str] =
    &["axon", "openai", "kimi", "glm", "anthropic"];

/// Adopter-facing acceptable values for `keepalive:` field.
pub const AXONENDPOINT_KEEPALIVE_VALUES: &[&str] = &["5s", "15s", "30s", "60s"];

/// §Fase 32.b D3 — Closed method enum for `method:` field. Adopter-
/// declarable methods only; HEAD/OPTIONS/CONNECT/TRACE are
/// runtime-managed (CORS preflight, etc.) and never declared from
/// source. Closed enum refuses interpretation drift; smart-suggest
/// catches near-misses at parse time.
pub const AXONENDPOINT_METHOD_VALUES: &[&str] = &["GET", "POST", "PUT", "DELETE", "PATCH"];

/// §Fase 36.d (D2) — Closed catalog for the `axonendpoint backend:`
/// declaration. The set is `CANONICAL_PROVIDERS ∪ {auto, stub}`:
///
///   - the seven canonical LLM providers — `anthropic`, `gemini`,
///     `glm`, `kimi`, `ollama`, `openai`, `openrouter` — a concrete,
///     declared backend that rung 2 of the Fase 36 D1 resolution
///     ladder fires immediately;
///   - `auto` — transparent: declaring it is equivalent to omitting
///     `backend:` entirely (the route resolves down the ladder —
///     server default → environment-available providers);
///   - `stub` — the no-op backend, reachable ONLY by an explicit,
///     written declaration (D5: a silent degradation to `stub` is
///     forbidden; an explicit opt-in is not).
///
/// `axon-frontend` carries zero runtime deps and therefore cannot
/// import `axon::backends::CANONICAL_PROVIDERS`; this list is a
/// hand-maintained mirror. The axon-rs drift gate
/// (`tests/fase36_d_backend_catalog_drift.rs`) asserts the two stay
/// byte-identical — adding a provider in one place without the other
/// fails CI.
pub const AXONENDPOINT_BACKEND_VALUES: &[&str] = &[
    "anthropic",
    "auto",
    "gemini",
    "glm",
    "kimi",
    "ollama",
    "openai",
    "openrouter",
    "stub",
];

#[inline]
fn axonendpoint_is_valid_transport(s: &str) -> bool {
    AXONENDPOINT_TRANSPORT_VALUES.iter().any(|&v| v == s)
}

#[inline]
fn axonendpoint_is_valid_method(s: &str) -> bool {
    AXONENDPOINT_METHOD_VALUES.iter().any(|&v| v == s)
}

#[inline]
fn axonendpoint_is_valid_backend(s: &str) -> bool {
    AXONENDPOINT_BACKEND_VALUES.iter().any(|&v| v == s)
}

#[inline]
fn axonendpoint_is_valid_keepalive(s: &str) -> bool {
    AXONENDPOINT_KEEPALIVE_VALUES.iter().any(|&v| v == s)
}

/// §Fase 37.y (D2) — Closed type catalog for query parameters.
///
/// Query values arrive over HTTP as URL-encoded strings; the catalog
/// is the set of types axon will validate / coerce them into for the
/// Request Binding Contract. Hand-curated, intentionally small:
///   - `Text` — the raw string (always succeeds)
///   - `Int` — `i64` parseable
///   - `Float` — `f64` parseable, finite
///   - `Bool` — case-insensitive `{true, false, 1, 0, yes, no, on, off}`
///   - `Uuid` — RFC 4122 textual form
///
/// Extending the catalog is a future axon-T?nn surface; v1.38.5 ships
/// the 5 types covering ~95% of REST query patterns. Lists / dates /
/// datetimes / enums are honest deferrals (see §7 of the plan vivo).
pub const AXONENDPOINT_QUERY_PARAM_TYPES: &[&str] =
    &["Text", "Int", "Float", "Bool", "Uuid"];

/// `true` iff `s` is one of the §Fase 37.y (D2) query-param catalog
/// entries — exact case-sensitive match (axon types are PascalCase).
#[inline]
pub(crate) fn axonendpoint_is_valid_query_param_type(s: &str) -> bool {
    AXONENDPOINT_QUERY_PARAM_TYPES.iter().any(|&v| v == s)
}

/// §Fase 37.y (D1) — Extract `{name}` placeholder names from an
/// `axonendpoint` `path:` string, in left-to-right declaration order.
///
/// Recognized placeholder grammar (single-segment, no nested braces):
/// `{NAME}` where `NAME` matches `[A-Za-z_][A-Za-z0-9_]*`. Anything
/// inside braces that does NOT match the identifier shape is silently
/// IGNORED — it's either an adopter typo (caught later by axum at
/// route registration) or a literal brace in the URL pattern.
///
/// Returns `Err(duplicate_name)` when the same `{name}` appears more
/// than once in the path — HTTP route patterns reject duplicates
/// structurally (`axum` would panic at registration), so surfacing
/// the error at parse time is the right place.
///
/// Pure + total: never panics; deterministic over its single string
/// argument. Hand-rolled scanner (no regex dep at parser layer).
///
/// # Examples
///
/// - `"/api/users"` → `Ok(vec![])`
/// - `"/api/users/{id}"` → `Ok(vec!["id"])`
/// - `"/api/tenants/{tenant_id}/secrets/{secret_name}"`
///   → `Ok(vec!["tenant_id", "secret_name"])`
/// - `"/api/users/{id}/posts/{id}"` → `Err("id")` (duplicate)
/// - `"/api/{not valid}"` → `Ok(vec![])` (malformed brace content
///   silently ignored; axum surfaces the error at registration)
pub(crate) fn extract_path_param_names(path: &str) -> Result<Vec<String>, String> {
    let mut out: Vec<String> = Vec::new();
    let bytes = path.as_bytes();
    let mut i = 0;
    while i < bytes.len() {
        if bytes[i] != b'{' {
            i += 1;
            continue;
        }
        // Find the matching close brace; if none, the open brace is
        // a literal — leave it alone.
        let start = i + 1;
        let mut end = start;
        while end < bytes.len() && bytes[end] != b'}' {
            end += 1;
        }
        if end == bytes.len() {
            // Unterminated — give up; downstream parser/runtime
            // surface the malformed path elsewhere.
            break;
        }
        let raw = &path[start..end];
        // Validate identifier shape: [A-Za-z_][A-Za-z0-9_]*
        let valid = !raw.is_empty()
            && raw.bytes().enumerate().all(|(idx, b)| {
                if idx == 0 {
                    b.is_ascii_alphabetic() || b == b'_'
                } else {
                    b.is_ascii_alphanumeric() || b == b'_'
                }
            });
        if valid {
            let name = raw.to_string();
            if out.iter().any(|existing| existing == &name) {
                return Err(name);
            }
            out.push(name);
        }
        i = end + 1;
    }
    Ok(out)
}

/// §Fase 32.g (D8) — Closed capability-slug grammar. Validates a
/// `requires:` slug per `^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$`.
///
/// Hand-rolled (no regex dep at parser layer) — each segment must
/// match `[a-z][a-z0-9_]*` and segments are joined by single dots.
/// Public so the runtime mirror (`axon::auth_scope`) reuses the same
/// predicate without duplicating the rule.
///
/// Examples valid: `admin`, `legal.read`, `hipaa.phi.read`,
/// `bank.officer.senior`, `a`, `a_b`, `a1`.
/// Examples invalid: empty, `Admin` (uppercase), `1admin` (digit
/// first), `bank-officer` (hyphen), `bank..a` (empty segment),
/// `.admin`, `admin.`, `admin..` .
pub fn is_valid_capability_slug(slug: &str) -> bool {
    if slug.is_empty() {
        return false;
    }
    for segment in slug.split('.') {
        if !is_valid_slug_segment(segment) {
            return false;
        }
    }
    true
}

fn is_valid_slug_segment(seg: &str) -> bool {
    let mut chars = seg.chars();
    let first = match chars.next() {
        Some(c) => c,
        None => return false,
    };
    if !first.is_ascii_lowercase() {
        return false;
    }
    chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
}

// ════════════════════════════════════════════════════════════════════
//  §Fase 37.y (D1) — `extract_path_param_names` unit tests
// ════════════════════════════════════════════════════════════════════

// ════════════════════════════════════════════════════════════════════
//  §Fase 37.y (D2) — `axonendpoint_is_valid_query_param_type` + the
//  inline `query: { … }` parser, end-to-end through the lexer.
// ════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod query_param_catalog_tests {
    use super::{axonendpoint_is_valid_query_param_type, AXONENDPOINT_QUERY_PARAM_TYPES};

    #[test]
    fn accepts_every_catalog_entry() {
        for ty in AXONENDPOINT_QUERY_PARAM_TYPES {
            assert!(
                axonendpoint_is_valid_query_param_type(ty),
                "catalog entry `{ty}` must validate"
            );
        }
    }

    #[test]
    fn rejects_off_catalog_types() {
        for off in &[
            "Timestamp",    // not in v1.38.5 — list/dates deferred
            "Date",
            "DateTime",
            "List<Text>",   // multi-value query params deferred (§7)
            "Jsonb",        // store-only types not query-applicable
            "Bytea",
            "text",         // lowercase rejected (axon types are PascalCase)
            "TEXT",
            "Number",       // not in axon's type catalog at all
            "",             // empty
            " ",            // whitespace
        ] {
            assert!(
                !axonendpoint_is_valid_query_param_type(off),
                "off-catalog `{off}` must reject"
            );
        }
    }

    #[test]
    fn catalog_size_matches_design() {
        // The plan vivo D2 states a closed 5-type catalog. A future
        // axon-T?nn surface may extend it; that requires updating BOTH
        // the catalog AND the plan vivo §7 honest-scope note.
        assert_eq!(AXONENDPOINT_QUERY_PARAM_TYPES.len(), 5);
    }
}

#[cfg(test)]
mod query_param_parser_tests {
    use crate::lexer::Lexer;
    use crate::parser::Parser;

    fn parse_endpoint_source(src: &str) -> Result<crate::ast::AxonEndpointDefinition, String> {
        let tokens = Lexer::new(src, "test.axon")
            .tokenize()
            .map_err(|e| format!("lex: {}", e.message))?;
        let mut parser = Parser::new(tokens);
        let program = parser.parse().map_err(|e| format!("parse: {}", e.message))?;
        program
            .declarations
            .into_iter()
            .find_map(|d| match d {
                crate::ast::Declaration::AxonEndpoint(e) => Some(e),
                _ => None,
            })
            .ok_or_else(|| "no axonendpoint in program".to_string())
    }

    #[test]
    fn endpoint_with_no_query_block_keeps_empty_vec() {
        let src = r#"
            axonendpoint write_secret {
                method: POST
                path: "/api/users"
                body: SecretWriteRequest
                execute: WriteSecret
            }
        "#;
        let ep = parse_endpoint_source(src).expect("parses");
        assert!(
            ep.query_params.is_empty(),
            "D5 — no `query:` block ⇒ empty query_params"
        );
    }

    #[test]
    fn single_query_param_required() {
        let src = r#"
            axonendpoint list_users {
                method: GET
                path: "/api/users"
                query: { status: Text }
                execute: ListUsers
            }
        "#;
        let ep = parse_endpoint_source(src).expect("parses");
        assert_eq!(ep.query_params.len(), 1);
        assert_eq!(ep.query_params[0].name, "status");
        assert_eq!(ep.query_params[0].type_expr.name, "Text");
        assert!(!ep.query_params[0].type_expr.optional);
    }

    #[test]
    fn optional_query_param_via_question_suffix() {
        let src = r#"
            axonendpoint list_users {
                method: GET
                path: "/api/users"
                query: { limit: Int? }
                execute: ListUsers
            }
        "#;
        let ep = parse_endpoint_source(src).expect("parses");
        assert_eq!(ep.query_params.len(), 1);
        assert_eq!(ep.query_params[0].name, "limit");
        assert_eq!(ep.query_params[0].type_expr.name, "Int");
        assert!(
            ep.query_params[0].type_expr.optional,
            "`?` suffix sets optional"
        );
    }

    #[test]
    fn multiple_query_params_preserve_declaration_order() {
        let src = r#"
            axonendpoint search {
                method: GET
                path: "/api/search"
                query: { q: Text, page: Int?, limit: Int?, exact: Bool? }
                execute: Search
            }
        "#;
        let ep = parse_endpoint_source(src).expect("parses");
        let names: Vec<&str> = ep.query_params.iter().map(|f| f.name.as_str()).collect();
        assert_eq!(names, vec!["q", "page", "limit", "exact"]);
        let types: Vec<&str> = ep
            .query_params
            .iter()
            .map(|f| f.type_expr.name.as_str())
            .collect();
        assert_eq!(types, vec!["Text", "Int", "Int", "Bool"]);
        let optionals: Vec<bool> = ep
            .query_params
            .iter()
            .map(|f| f.type_expr.optional)
            .collect();
        assert_eq!(optionals, vec![false, true, true, true]);
    }

    #[test]
    fn duplicate_query_param_is_parse_error() {
        let src = r#"
            axonendpoint bad {
                method: GET
                path: "/api/x"
                query: { name: Text, name: Int? }
                execute: Bad
            }
        "#;
        let err = parse_endpoint_source(src).expect_err("must fail");
        assert!(
            err.contains("duplicate query param 'name'"),
            "error must name the duplicate. Got: {err}"
        );
    }

    #[test]
    fn off_catalog_type_with_smart_suggest_hint() {
        // `Strng` is one edit away from `Text` (would suggest `Text`?
        // Actually edit distance to `Text` is 4; to `Int` is 5. Likely
        // no smart suggestion within distance 2. The error still names
        // the catalog explicitly.)
        let src = r#"
            axonendpoint bad {
                method: GET
                path: "/api/x"
                query: { value: Strng }
                execute: Bad
            }
        "#;
        let err = parse_endpoint_source(src).expect_err("must fail");
        assert!(
            err.contains("unsupported type 'Strng'"),
            "error must name the bad type. Got: {err}"
        );
        assert!(
            err.contains("Expected one of: Text | Int | Float | Bool | Uuid"),
            "error must list the closed catalog. Got: {err}"
        );
    }

    #[test]
    fn close_typo_gets_did_you_mean_hint() {
        // `Txt` → edit distance 1 from `Text` → smart-suggest should
        // surface the hint.
        let src = r#"
            axonendpoint bad {
                method: GET
                path: "/api/x"
                query: { value: Txt }
                execute: Bad
            }
        "#;
        let err = parse_endpoint_source(src).expect_err("must fail");
        assert!(
            err.contains("Did you mean") && err.contains("`Text`"),
            "smart-suggest must hint `Text`. Got: {err}"
        );
    }

    #[test]
    fn every_catalog_type_parses_cleanly() {
        // Round-trip smoke for all 5 catalog entries.
        for ty in &["Text", "Int", "Float", "Bool", "Uuid"] {
            let src = format!(
                r#"
                    axonendpoint x {{
                        method: GET
                        path: "/api/x"
                        query: {{ v: {ty} }}
                        execute: X
                    }}
                "#
            );
            let ep = parse_endpoint_source(&src)
                .unwrap_or_else(|e| panic!("`{ty}` should parse: {e}"));
            assert_eq!(ep.query_params[0].type_expr.name, *ty);
        }
    }

    #[test]
    fn comma_optional_between_params() {
        // The plan vivo design accepts both comma-separated and
        // whitespace-separated query params (existing parser style is
        // forgiving). Whitespace-only:
        let src = r#"
            axonendpoint x {
                method: GET
                path: "/api/x"
                query: { a: Text b: Int? }
                execute: X
            }
        "#;
        let ep = parse_endpoint_source(src).expect("parses without commas");
        assert_eq!(ep.query_params.len(), 2);
    }

    // ─── Robustness hardening (37.y.2 100% robust closure) ──────────

    #[test]
    fn double_query_block_is_parse_error() {
        // An adopter who copy-pastes the `query:` block twice should
        // see a clear parse error, not a silent merge that produces
        // an unexpectedly-augmented endpoint with both blocks fused.
        let src = r#"
            axonendpoint x {
                method: GET
                path: "/api/x"
                query: { a: Text }
                query: { b: Int? }
                execute: X
            }
        "#;
        let err = parse_endpoint_source(src).expect_err("must fail");
        assert!(
            err.contains("declares `query: { … }` more than once"),
            "error must call out the duplicate block. Got: {err}"
        );
        assert!(
            err.contains("combine all params into a single block"),
            "error must hint the canonical fix. Got: {err}"
        );
    }

    #[test]
    fn optional_generic_type_is_parse_error_with_canonical_hint() {
        // `Optional<Text>` is the wrong way to declare an optional
        // query param. The canonical syntax is `Text?` (the `?`
        // suffix). The error must surface this with a literal example.
        let src = r#"
            axonendpoint x {
                method: GET
                path: "/api/x"
                query: { value: Optional<Text> }
                execute: X
            }
        "#;
        let err = parse_endpoint_source(src).expect_err("must fail");
        assert!(
            err.contains("generic type `Optional<Text>`"),
            "error must name the generic type literally. Got: {err}"
        );
        assert!(
            err.contains("Use `Text?` (the `?` suffix)"),
            "error must hint the canonical `Text?` syntax. Got: {err}"
        );
    }

    #[test]
    fn list_generic_type_is_parse_error_with_deferral_hint() {
        // Multi-value query params (`?tag=a&tag=b`) are honest-
        // deferred per the plan vivo §7. Adopters who write
        // `List<Text>` should see a clear error explaining the
        // deferral, not a confusing "type `List` not in catalog".
        let src = r#"
            axonendpoint x {
                method: GET
                path: "/api/x"
                query: { tags: List<Text> }
                execute: X
            }
        "#;
        let err = parse_endpoint_source(src).expect_err("must fail");
        assert!(
            err.contains("generic type `List<Text>`"),
            "error must name the generic type. Got: {err}"
        );
        assert!(
            err.contains("Multi-value query params")
                && err.contains("honest-deferred"),
            "error must mention the multi-value deferral. Got: {err}"
        );
    }

    #[test]
    fn other_generic_types_caught_generically() {
        // Generic types beyond `Optional` and `List` get the
        // generic-rejection message without a canonical-syntax hint
        // (the catalog list is the canonical guidance).
        let src = r#"
            axonendpoint x {
                method: GET
                path: "/api/x"
                query: { value: Stream<Int> }
                execute: X
            }
        "#;
        let err = parse_endpoint_source(src).expect_err("must fail");
        assert!(
            err.contains("generic type `Stream<Int>`"),
            "error must name the generic type. Got: {err}"
        );
        assert!(
            err.contains("Text | Int | Float | Bool | Uuid"),
            "error must list the closed catalog. Got: {err}"
        );
    }

    #[test]
    fn uuid_optional_parses_cleanly() {
        // Hardening companion — `Uuid?` is in the catalog AND
        // optional. The two features compose without surprise.
        let src = r#"
            axonendpoint find {
                method: GET
                path: "/api/x"
                query: { after: Uuid? }
                execute: Find
            }
        "#;
        let ep = parse_endpoint_source(src).expect("parses");
        assert_eq!(ep.query_params.len(), 1);
        assert_eq!(ep.query_params[0].name, "after");
        assert_eq!(ep.query_params[0].type_expr.name, "Uuid");
        assert!(ep.query_params[0].type_expr.optional);
        assert_eq!(ep.query_params[0].type_expr.generic_param, "");
    }

    #[test]
    fn empty_query_block_yields_empty_vec() {
        // `query: { }` is grammatically valid but semantically a
        // no-op (equivalent to omitting the block). Don't error;
        // just record an empty Vec.
        let src = r#"
            axonendpoint x {
                method: GET
                path: "/api/x"
                query: { }
                execute: X
            }
        "#;
        let ep = parse_endpoint_source(src).expect("empty block parses");
        assert!(ep.query_params.is_empty());
    }

    #[test]
    fn kivi_secret_write_path_plus_query() {
        // Combined path-param + query-param test: an endpoint that
        // takes IDs in the URL AND optional filters in the query
        // string. This is the natural REST shape Fase 37.y serves.
        let src = r#"
            axonendpoint write_secret {
                method: POST
                path: "/api/tenants/{tenant_id}/secrets/{secret_name}"
                query: { dry_run: Bool?, overwrite: Bool? }
                body: SecretWriteRequest
                execute: WriteSecret
            }
        "#;
        let ep = parse_endpoint_source(src).expect("parses");
        // Path params populated (from 37.y.1):
        assert_eq!(ep.path_params, vec!["tenant_id", "secret_name"]);
        // Query params populated (from this sub-fase 37.y.2):
        assert_eq!(ep.query_params.len(), 2);
        assert_eq!(ep.query_params[0].name, "dry_run");
        assert_eq!(ep.query_params[0].type_expr.name, "Bool");
        assert!(ep.query_params[0].type_expr.optional);
        assert_eq!(ep.query_params[1].name, "overwrite");
        // Body still works:
        assert_eq!(ep.body_type, "SecretWriteRequest");
    }
}

#[cfg(test)]
mod path_param_extraction_tests {
    use super::extract_path_param_names;

    #[test]
    fn empty_path_no_placeholders() {
        assert_eq!(extract_path_param_names("/api/users"), Ok(vec![]));
        assert_eq!(extract_path_param_names("/"), Ok(vec![]));
        assert_eq!(extract_path_param_names(""), Ok(vec![]));
    }

    #[test]
    fn single_placeholder() {
        assert_eq!(
            extract_path_param_names("/api/users/{id}"),
            Ok(vec!["id".to_string()])
        );
    }

    #[test]
    fn multiple_placeholders_in_declaration_order() {
        assert_eq!(
            extract_path_param_names(
                "/api/tenants/{tenant_id}/secrets/{secret_name}"
            ),
            Ok(vec![
                "tenant_id".to_string(),
                "secret_name".to_string(),
            ])
        );
    }

    #[test]
    fn kivi_chat_history_path_pattern() {
        // The exact pattern the kivi adopter reported (2026-05-20):
        // POST /api/tenants/{tenant_id}/secrets/{secret_name}
        // Both names extracted in source order.
        let names = extract_path_param_names(
            "/api/tenants/{tenant_id}/secrets/{secret_name}",
        );
        assert_eq!(
            names,
            Ok(vec![
                "tenant_id".to_string(),
                "secret_name".to_string(),
            ])
        );
    }

    #[test]
    fn duplicate_placeholder_returns_err() {
        assert_eq!(
            extract_path_param_names("/api/users/{id}/posts/{id}"),
            Err("id".to_string())
        );
    }

    #[test]
    fn underscore_and_numeric_in_name() {
        assert_eq!(
            extract_path_param_names("/api/{user_id}/items/{item_2}"),
            Ok(vec!["user_id".to_string(), "item_2".to_string()])
        );
    }

    #[test]
    fn leading_underscore_accepted() {
        // Identifiers in HTTP paths often start with letters but the
        // grammar permits leading underscore (parity with Rust identifier
        // rules). The flow parameter name on the binding side has to
        // match exactly, so adopters with `_internal_id` in the path
        // can pair it with a same-named flow param.
        assert_eq!(
            extract_path_param_names("/api/{_internal}"),
            Ok(vec!["_internal".to_string()])
        );
    }

    #[test]
    fn malformed_placeholder_silently_ignored() {
        // Content inside `{...}` that does not match the identifier
        // grammar is skipped at this layer. axum surfaces the route
        // registration failure if the literal text is invalid.
        assert_eq!(
            extract_path_param_names("/api/{not valid}"),
            Ok(vec![])
        );
        // Empty braces — same: skip silently.
        assert_eq!(extract_path_param_names("/api/{}"), Ok(vec![]));
        // Mixed: malformed brace skipped, valid placeholder kept.
        assert_eq!(
            extract_path_param_names("/api/{tenant id}/users/{id}"),
            Ok(vec!["id".to_string()])
        );
    }

    #[test]
    fn unterminated_brace_returns_clean() {
        // Open brace with no close brace — give up without panicking.
        // (axum surfaces the malformed-route error at registration.)
        assert_eq!(extract_path_param_names("/api/{id"), Ok(vec![]));
    }

    #[test]
    fn placeholders_at_path_boundaries() {
        // Placeholder as the very first segment AND the very last
        // segment — both should be extracted.
        assert_eq!(
            extract_path_param_names("{prefix}/api/users/{id}"),
            Ok(vec!["prefix".to_string(), "id".to_string()])
        );
        assert_eq!(
            extract_path_param_names("/api/{id}"),
            Ok(vec!["id".to_string()])
        );
    }

    #[test]
    fn deduplication_detects_non_adjacent_duplicates() {
        // The duplicate-detection sweep is global, not just adjacent.
        assert_eq!(
            extract_path_param_names(
                "/api/orgs/{org_id}/teams/{team_id}/repos/{org_id}"
            ),
            Err("org_id".to_string())
        );
    }

    #[test]
    fn never_panics_on_arbitrary_input() {
        // Light fuzz: a handful of weird inputs return cleanly.
        for input in &[
            "{",
            "}",
            "{}",
            "{{}}",
            "{{{",
            "/api/{}/{id}",
            "////",
            "\u{1F4A1}",        // emoji (lightbulb)
            "\u{0000}",         // null byte
        ] {
            let _ = extract_path_param_names(input); // must not panic
        }
    }
}

#[cfg(test)]
mod capability_slug_tests {
    use super::is_valid_capability_slug;

    #[test]
    fn accepts_canonical_examples() {
        assert!(is_valid_capability_slug("admin"));
        assert!(is_valid_capability_slug("legal.read"));
        assert!(is_valid_capability_slug("hipaa.phi.read"));
        assert!(is_valid_capability_slug("bank.officer.senior"));
        assert!(is_valid_capability_slug("a"));
        assert!(is_valid_capability_slug("a_b"));
        assert!(is_valid_capability_slug("a1"));
        assert!(is_valid_capability_slug("a.b1_c"));
    }

    #[test]
    fn rejects_empty_string() {
        assert!(!is_valid_capability_slug(""));
    }

    #[test]
    fn rejects_uppercase() {
        assert!(!is_valid_capability_slug("Admin"));
        assert!(!is_valid_capability_slug("admin.READ"));
    }

    #[test]
    fn rejects_digit_first() {
        assert!(!is_valid_capability_slug("1admin"));
        assert!(!is_valid_capability_slug("admin.1read"));
    }

    #[test]
    fn rejects_hyphen() {
        assert!(!is_valid_capability_slug("bank-officer"));
    }

    #[test]
    fn rejects_empty_segments() {
        assert!(!is_valid_capability_slug("bank..a"));
        assert!(!is_valid_capability_slug(".admin"));
        assert!(!is_valid_capability_slug("admin."));
    }

    #[test]
    fn rejects_special_chars() {
        assert!(!is_valid_capability_slug("admin@read"));
        assert!(!is_valid_capability_slug("admin/read"));
        assert!(!is_valid_capability_slug("admin read"));
    }
}

// ── Parser ───────────────────────────────────────────────────────────────────

pub struct Parser {
    tokens: Vec<Token>,
    pos: usize,
    /// Fase 14.a — leading trivia parallel array, indexed by the
    /// effective-token position. `leading_trivia[i]` is the comment
    /// trivia that appeared between the previous effective token (or
    /// file start) and `tokens[i]`.
    leading_trivia: Vec<Vec<Trivia>>,
    /// Fase 14.a — trailing trivia parallel array. `trailing_trivia[i]`
    /// is the comment trivia on the same line as `tokens[i]`, before
    /// the next effective token. Populated by the constructor.
    trailing_trivia: Vec<Vec<Trivia>>,
    /// Fase 17.a — side-channel for tagging let value_kind. Set by
    /// `parse_let_atom` / `parse_let_value_expr` as they descend; read
    /// at the end of `parse_let` and stored on the LetStatement.
    last_let_value_kind: String,
    /// Fase 19.e — loop nesting depth for break/continue scope check.
    /// Incremented at the start of `parse_for_in`, decremented after.
    /// `parse_break`/`parse_continue` raise ParseError when this is
    /// zero (the keyword has no meaning outside a loop body).
    loop_depth: u32,
    /// §Fase 28.d — Optional source text + filename for the rustc-
    /// style source-context block on `ParseError`. Set via the
    /// fluent `Parser::with_source` builder; default `None` keeps
    /// existing callers (`Parser::new(tokens).parse()`) emitting
    /// the legacy single-line shape.
    source: Option<String>,
    filename: String,
}

impl Parser {
    pub fn new(raw_tokens: Vec<Token>) -> Self {
        // ── Fase 14.a — split the raw token stream into:
        //   - effective tokens the grammar consumes (cursor advances
        //     over these as before),
        //   - parallel `leading_trivia` / `trailing_trivia` arrays
        //     indexed by effective-token position.
        // Comments on a fresh line attach as leading trivia of the
        // next effective token; comments on the same line as an
        // effective token attach as trailing trivia of that token.
        // Roslyn/Swift convention.
        let mut effective: Vec<Token> = Vec::with_capacity(raw_tokens.len());
        let mut leading: Vec<Vec<Trivia>> = Vec::with_capacity(raw_tokens.len());
        let mut trailing: Vec<Vec<Trivia>> = Vec::with_capacity(raw_tokens.len());

        let mut pending_leading: Vec<Trivia> = Vec::new();
        let mut last_effective_line: i64 = -1;
        for tok in raw_tokens {
            if is_comment_token(&tok.ttype) {
                let kind = token_to_trivia_kind(&tok.ttype)
                    .expect("comment token must map to a trivia kind");
                let triv = Trivia {
                    kind,
                    text: tok.value,
                    line: tok.line,
                    column: tok.column,
                };
                if !effective.is_empty() && (tok.line as i64) == last_effective_line {
                    trailing.last_mut().unwrap().push(triv);
                } else {
                    pending_leading.push(triv);
                }
            } else {
                last_effective_line = tok.line as i64;
                effective.push(tok);
                leading.push(std::mem::take(&mut pending_leading));
                trailing.push(Vec::new());
            }
        }

        Parser {
            tokens: effective,
            pos: 0,
            leading_trivia: leading,
            trailing_trivia: trailing,
            last_let_value_kind: "literal".to_string(),
            loop_depth: 0,
            source: None,
            filename: "<source>".to_string(),
        }
    }

    /// §Fase 28.d — Fluent attach of source text + filename for
    /// rustc-style source-context blocks on emitted `ParseError`s.
    /// Returns `self` so it chains with `.parse_with_recovery()`:
    ///
    /// ```ignore
    /// let result = Parser::new(tokens)
    ///     .with_source(src, "foo.axon")
    ///     .parse_with_recovery();
    /// ```
    ///
    /// No-op of any other behaviour — pure metadata attach.
    #[must_use]
    pub fn with_source(mut self, source: &str, filename: &str) -> Self {
        self.source = Some(source.to_string());
        self.filename = filename.to_string();
        self
    }

    // ── public API ───────────────────────────────────────────────

    pub fn parse(&mut self) -> Result<Program, ParseError> {
        let mut program = Program {
            declarations: Vec::new(),
            declaration_trivia: Vec::new(),
            loc: Loc { line: 1, column: 1 },
        };
        while !self.check(TokenType::Eof) {
            // Capture trivia around the declaration. `start_pos` is
            // the effective-token position of the declaration's first
            // token; that position carries the leading trivia. After
            // parsing, `pos - 1` is the last token consumed; that
            // position carries the trailing trivia.
            let start_pos = self.pos;
            let mut decl = match self.parse_declaration() {
                Ok(d) => d,
                Err(e) => return Err(self.attach_source_to_error(e)),
            };
            let end_pos = self.pos.saturating_sub(1);
            let leading = self
                .leading_trivia
                .get(start_pos)
                .cloned()
                .unwrap_or_default();
            let trailing = self
                .trailing_trivia
                .get(end_pos)
                .cloned()
                .unwrap_or_default();
            // Fase 14.b — also copy trivia into the per-struct fields on
            // the declaration so consumers can read `flow.leading_trivia`
            // directly without going through `program.declaration_trivia[i]`.
            // The side-channel is preserved for backward compat with
            // 14.a callers and as a flat enumeration source.
            attach_trivia_to_decl(&mut decl, leading.clone(), trailing.clone());
            program.declarations.push(decl);
            program
                .declaration_trivia
                .push(DeclarationTrivia { leading, trailing });
        }
        Ok(program)
    }

    // ── §Fase 28.c — recovery-mode parse ─────────────────────────
    //
    // Mirror of Python's `Parser.parse_with_recovery` from
    // `axon/compiler/parser.py`. Wraps `parse_declaration` in a
    // try/recover loop: on any `ParseError` the error is appended to
    // the list and the cursor advances to the next sync point, then
    // parsing resumes. The two stacks must produce structurally
    // identical error lists on the same input — that is the cross-
    // stack drift gate (D7). See the test module
    // `tests::fase28_recovery_tests` and Python-side
    // `tests/test_fase28_parser_recovery.py`.

    /// Recovery-mode parse. Collects every parse error in source
    /// order; the existing `parse()` API remains fail-fast (D9).
    ///
    /// # Recovery contract (D2)
    ///
    /// On `ParseError`:
    ///   1. Push the error onto `errors`.
    ///   2. If the cursor is already on a top-level declaration
    ///      keyword (and brace-depth ≤ 0), do not consume — the
    ///      caller should retry the declaration parse from here.
    ///      Otherwise advance one token to make progress, then
    ///      walk to the next sync point.
    ///   3. Resume the outer loop.
    ///
    /// Sync points: top-level declaration keyword at brace-depth ≤ 0,
    /// or EOF. Negative depths are treated identically to ≤ 0 — the
    /// walker keeps walking through over-balanced `}` rather than
    /// pretending a closing brace is itself a sync point (which would
    /// emit a ghost "Unexpected token at top level" error in the
    /// outer loop).
    pub fn parse_with_recovery(&mut self) -> ParseResult {
        let mut program = Program {
            declarations: Vec::new(),
            declaration_trivia: Vec::new(),
            loc: Loc { line: 1, column: 1 },
        };
        let mut errors: Vec<ParseError> = Vec::new();

        while !self.check(TokenType::Eof) {
            let start_pos = self.pos;
            match self.parse_declaration() {
                Ok(mut decl) => {
                    let end_pos = self.pos.saturating_sub(1);
                    let leading = self
                        .leading_trivia
                        .get(start_pos)
                        .cloned()
                        .unwrap_or_default();
                    let trailing = self
                        .trailing_trivia
                        .get(end_pos)
                        .cloned()
                        .unwrap_or_default();
                    attach_trivia_to_decl(&mut decl, leading.clone(), trailing.clone());
                    program.declarations.push(decl);
                    program
                        .declaration_trivia
                        .push(DeclarationTrivia { leading, trailing });
                }
                Err(err) => {
                    // §Fase 28.d — attach source-context block when a
                    // source has been provided via `with_source(...)`;
                    // otherwise the error keeps its single-line shape.
                    errors.push(self.attach_source_to_error(err));
                    // Make progress. If parse_declaration returned
                    // immediately on the same token (e.g. unknown
                    // top-level token), we MUST advance at least one
                    // token to avoid an infinite loop.
                    if self.pos == start_pos && !self.check(TokenType::Eof) {
                        self.advance();
                    }
                    self.advance_to_sync_point();
                }
            }
        }

        ParseResult { program, errors }
    }

    /// §Fase 28.d — Decorate a `ParseError` with a `SourceSnippet`
    /// when the parser has source context attached, otherwise return
    /// the error unchanged. Idempotent: if the error already carries
    /// a snippet, this overwrites it with the parser's source.
    fn attach_source_to_error(&self, err: ParseError) -> ParseError {
        match &self.source {
            Some(src) if err.line >= 1 => err.attach_source(src, &self.filename),
            _ => err,
        }
    }

    /// §Fase 28.c — Walk the cursor forward until the next sync
    /// point (top-level declaration keyword at brace-depth ≤ 0) or
    /// EOF. Used by `parse_with_recovery` to skip the malformed
    /// remainder of a failed declaration.
    fn advance_to_sync_point(&mut self) {
        let mut depth: i32 = 0;
        while !self.check(TokenType::Eof) {
            let tt = self.current().ttype.clone();
            // Sync at top-level keywords when depth ≤ 0. We do not
            // consume the keyword — the outer loop will dispatch on
            // it.
            if is_top_level_decl_kw_for_recovery(&tt) && depth <= 0 {
                return;
            }
            if matches!(tt, TokenType::LBrace) {
                depth += 1;
            } else if matches!(tt, TokenType::RBrace) {
                depth -= 1;
            }
            self.advance();
        }
    }

    // ── token helpers ────────────────────────────────────────────

    fn current(&self) -> &Token {
        if self.pos >= self.tokens.len() {
            self.tokens.last().unwrap() // EOF sentinel
        } else {
            &self.tokens[self.pos]
        }
    }

    fn advance(&mut self) -> &Token {
        let idx = self.pos;
        if self.pos < self.tokens.len() {
            self.pos += 1;
        }
        &self.tokens[idx]
    }

    fn check(&self, tt: TokenType) -> bool {
        self.current().ttype == tt
    }

    fn consume(&mut self, expected: TokenType) -> Result<Token, ParseError> {
        let tok = self.current().clone();
        if tok.ttype != expected {
            return Err(ParseError {
                message: format!(
                    "Expected {:?}, found {:?}('{}')",
                    expected, tok.ttype, tok.value
                ),
                line: tok.line,
                column: tok.column,
                            ..Default::default()
            });
        }
        self.pos += 1;
        Ok(tok)
    }

    /// §Fase 41.b — build a `ParseError` at the current token's location.
    fn error(&self, message: &str) -> ParseError {
        let tok = self.current();
        ParseError { message: message.to_string(), line: tok.line, column: tok.column, ..Default::default() }
    }

    /// Consume any identifier or keyword-used-as-value.
    fn consume_any_ident_or_kw(&mut self) -> Result<Token, ParseError> {
        let tok = self.current().clone();
        match tok.ttype {
            TokenType::Identifier
            | TokenType::Bool
            | TokenType::StringLit
            | TokenType::Integer
            | TokenType::Float => {
                self.pos += 1;
                Ok(tok)
            }
            _ => {
                // Allow any keyword token whose value is alphabetic
                if !tok.value.is_empty()
                    && tok.value.chars().all(|c| c.is_alphanumeric() || c == '_')
                    && tok.ttype != TokenType::Eof
                {
                    self.pos += 1;
                    Ok(tok)
                } else {
                    Err(ParseError {
                        message: format!(
                            "Expected identifier or keyword value, found {:?}('{}')",
                            tok.ttype, tok.value
                        ),
                        line: tok.line,
                        column: tok.column,
                                            ..Default::default()
                    })
                }
            }
        }
    }

    fn consume_number(&mut self) -> Result<f64, ParseError> {
        let tok = self.current().clone();
        match tok.ttype {
            TokenType::Float | TokenType::Integer => {
                self.pos += 1;
                tok.value.parse::<f64>().map_err(|_| ParseError {
                    message: format!("Invalid number '{}'", tok.value),
                    line: tok.line,
                    column: tok.column,
                                    ..Default::default()
                })
            }
            _ => Err(ParseError {
                message: format!("Expected number, found {:?}('{}')", tok.ttype, tok.value),
                line: tok.line,
                column: tok.column,
                            ..Default::default()
            }),
        }
    }

    fn parse_bool(&mut self) -> Result<bool, ParseError> {
        let tok = self.consume(TokenType::Bool)?;
        Ok(tok.value == "true")
    }

    fn loc_of(&self, tok: &Token) -> Loc {
        Loc {
            line: tok.line,
            column: tok.column,
        }
    }

    fn check_comparison(&self) -> bool {
        matches!(
            self.current().ttype,
            TokenType::Lt
                | TokenType::Gt
                | TokenType::Lte
                | TokenType::Gte
                | TokenType::Eq
                | TokenType::Neq
        )
    }

    fn check_run_modifier(&self) -> bool {
        matches!(
            self.current().ttype,
            TokenType::As
                | TokenType::Within
                | TokenType::ConstrainedBy
                | TokenType::OnFailure
                | TokenType::OutputTo
                | TokenType::Effort
        )
    }

    // ── list helpers ─────────────────────────────────────────────

    fn parse_string_list(&mut self) -> Result<Vec<String>, ParseError> {
        self.consume(TokenType::LBracket)?;
        let mut items = Vec::new();
        items.push(self.consume(TokenType::StringLit)?.value);
        while self.check(TokenType::Comma) {
            self.advance();
            items.push(self.consume(TokenType::StringLit)?.value);
        }
        self.consume(TokenType::RBracket)?;
        Ok(items)
    }

    fn parse_identifier_list(&mut self) -> Result<Vec<String>, ParseError> {
        let mut names = Vec::new();
        names.push(self.consume(TokenType::Identifier)?.value);
        while self.check(TokenType::Comma) {
            self.advance();
            names.push(self.consume(TokenType::Identifier)?.value);
        }
        Ok(names)
    }

    fn parse_bracketed_identifiers(&mut self) -> Result<Vec<String>, ParseError> {
        self.consume(TokenType::LBracket)?;
        let items = self.parse_extended_identifier_list()?;
        self.consume(TokenType::RBracket)?;
        Ok(items)
    }

    fn parse_extended_identifier_list(&mut self) -> Result<Vec<String>, ParseError> {
        let mut items = Vec::new();
        items.push(self.consume_any_ident_or_kw()?.value);
        while self.check(TokenType::Comma) {
            self.advance();
            items.push(self.consume_any_ident_or_kw()?.value);
        }
        Ok(items)
    }

    fn parse_dotted_identifier(&mut self) -> Result<String, ParseError> {
        let mut parts = vec![self.consume_any_ident_or_kw()?.value];
        while self.check(TokenType::Dot) {
            self.advance();
            parts.push(self.consume_any_ident_or_kw()?.value);
        }
        Ok(parts.join("."))
    }

    fn parse_expression_string(&mut self) -> Result<String, ParseError> {
        if self.check(TokenType::LBracket) {
            let items = self.parse_bracketed_dot_identifiers()?;
            return Ok(format!("[{}]", items.join(", ")));
        }
        self.parse_dotted_identifier()
    }

    fn parse_bracketed_dot_identifiers(&mut self) -> Result<Vec<String>, ParseError> {
        self.consume(TokenType::LBracket)?;
        let mut items = vec![self.parse_dotted_identifier()?];
        while self.check(TokenType::Comma) {
            self.advance();
            items.push(self.parse_dotted_identifier()?);
        }
        self.consume(TokenType::RBracket)?;
        Ok(items)
    }

    fn parse_argument_list(&mut self) -> Result<Vec<String>, ParseError> {
        let mut args = Vec::new();
        while !self.check(TokenType::RParen) {
            let tok = self.current().clone();
            match tok.ttype {
                TokenType::StringLit | TokenType::Integer | TokenType::Float => {
                    self.advance();
                    args.push(tok.value);
                }
                TokenType::Identifier => {
                    self.advance();
                    let mut val = tok.value;
                    if self.check(TokenType::Dot) {
                        self.advance();
                        val.push('.');
                        val.push_str(&self.consume_any_ident_or_kw()?.value);
                    }
                    args.push(val);
                }
                _ => {
                    self.advance();
                    let key = tok.value;
                    if self.check(TokenType::Colon) {
                        self.advance();
                        let v = self.advance().value.clone();
                        args.push(format!("{key}:{v}"));
                    } else {
                        args.push(key);
                    }
                }
            }
            if self.check(TokenType::Comma) {
                self.advance();
            }
        }
        Ok(args)
    }

    /// Skip a single value or balanced bracketed/braced block (unknown field).
    fn skip_value(&mut self) {
        match self.current().ttype {
            TokenType::LBracket => {
                self.advance();
                let mut depth = 1u32;
                while depth > 0 && !self.check(TokenType::Eof) {
                    if self.check(TokenType::LBracket) {
                        depth += 1;
                    } else if self.check(TokenType::RBracket) {
                        depth -= 1;
                    }
                    self.advance();
                }
            }
            TokenType::LBrace => {
                self.advance();
                let mut depth = 1u32;
                while depth > 0 && !self.check(TokenType::Eof) {
                    if self.check(TokenType::LBrace) {
                        depth += 1;
                    } else if self.check(TokenType::RBrace) {
                        depth -= 1;
                    }
                    self.advance();
                }
            }
            TokenType::Lt => {
                // effect row: <io, network, ...>
                self.advance();
                let mut depth = 1u32;
                while depth > 0 && !self.check(TokenType::Eof) {
                    if self.check(TokenType::Lt) {
                        depth += 1;
                    } else if self.check(TokenType::Gt) {
                        depth -= 1;
                    }
                    self.advance();
                }
            }
            _ => {
                self.advance();
                while self.check(TokenType::Dot) {
                    self.advance();
                    self.advance();
                }
            }
        }
    }

    /// Skip a balanced `{ ... }` block including its braces.
    fn skip_braced_block(&mut self) -> Result<(), ParseError> {
        self.consume(TokenType::LBrace)?;
        let mut depth = 1u32;
        while depth > 0 {
            if self.check(TokenType::Eof) {
                let tok = self.current();
                return Err(ParseError {
                    message: "Unterminated block — expected '}'".to_string(),
                    line: tok.line,
                    column: tok.column,
                                    ..Default::default()
                });
            }
            if self.check(TokenType::LBrace) {
                depth += 1;
            } else if self.check(TokenType::RBrace) {
                depth -= 1;
            }
            self.advance();
        }
        Ok(())
    }

    fn at_declaration_start(&self) -> bool {
        is_declaration_keyword(&self.current().ttype) || self.check(TokenType::Eof)
    }

    // ── top-level dispatch ───────────────────────────────────────

    fn parse_declaration(&mut self) -> Result<Declaration, ParseError> {
        let tok = self.current().clone();

        match tok.ttype {
            TokenType::Import => self.parse_import().map(Declaration::Import),
            TokenType::Persona => self.parse_persona().map(Declaration::Persona),
            TokenType::Context => self.parse_context().map(Declaration::Context),
            TokenType::Anchor => self.parse_anchor().map(Declaration::Anchor),
            TokenType::Memory => self.parse_memory().map(Declaration::Memory),
            TokenType::Tool => self.parse_tool().map(Declaration::Tool),
            TokenType::Type => self.parse_type_def().map(Declaration::Type),
            TokenType::Flow => self.parse_flow().map(Declaration::Flow),
            TokenType::Intent => self.parse_intent().map(Declaration::Intent),
            TokenType::Run => self.parse_run().map(Declaration::Run),
            TokenType::Let => self.parse_let().map(Declaration::Let),
            TokenType::Know | TokenType::Believe | TokenType::Speculate | TokenType::Doubt => {
                self.parse_epistemic_block().map(Declaration::Epistemic)
            }
            TokenType::Lambda => self.parse_lambda_data().map(Declaration::LambdaData),

            // ── Tier 2 declarations (full AST) ──────────────────
            TokenType::Agent => self.parse_agent().map(Declaration::Agent),
            TokenType::Shield => self.parse_shield().map(Declaration::Shield),
            TokenType::Pix => self.parse_pix().map(Declaration::Pix),
            TokenType::Psyche => self.parse_psyche().map(Declaration::Psyche),
            TokenType::Corpus => self.parse_corpus().map(Declaration::Corpus),
            TokenType::Dataspace => self.parse_dataspace().map(Declaration::Dataspace),
            TokenType::Ots => self.parse_ots().map(Declaration::Ots),
            TokenType::Mandate => self.parse_mandate().map(Declaration::Mandate),
            TokenType::Compute => self.parse_compute().map(Declaration::Compute),
            TokenType::Daemon => self.parse_daemon().map(Declaration::Daemon),
            TokenType::Extension => self.parse_extension().map(Declaration::Extension),
            TokenType::AxonStore => self.parse_axonstore().map(Declaration::AxonStore),
            TokenType::AxonEndpoint => self.parse_axonendpoint().map(Declaration::AxonEndpoint),

            // ── §λ-L-E Fase 1 — I/O cognitivo ───────────────────
            TokenType::Resource => self.parse_resource().map(Declaration::Resource),
            TokenType::Fabric => self.parse_fabric().map(Declaration::Fabric),
            TokenType::Manifest => self.parse_manifest().map(Declaration::Manifest),
            TokenType::Observe => self.parse_observe().map(Declaration::Observe),

            // ── §λ-L-E Fase 3 — Control cognitivo ───────────────
            TokenType::Reconcile => self.parse_reconcile().map(Declaration::Reconcile),
            TokenType::Lease => self.parse_lease().map(Declaration::Lease),
            TokenType::Ensemble => self.parse_ensemble().map(Declaration::Ensemble),

            // ── §λ-L-E Fase 4 — Topology + π-calculus sessions ─
            TokenType::Session => self.parse_session_definition().map(Declaration::Session),
            TokenType::Topology => self.parse_topology().map(Declaration::Topology),

            // ── §Fase 41.b — typed WebSocket transport ─────────
            TokenType::Socket => self.parse_socket().map(Declaration::Socket),

            // ── §λ-L-E Fase 5 — Cognitive immune system ─────────
            TokenType::Immune => self.parse_immune().map(Declaration::Immune),
            TokenType::Reflex => self.parse_reflex().map(Declaration::Reflex),
            TokenType::Heal => self.parse_heal().map(Declaration::Heal),

            // ── §λ-L-E Fase 9 — UI cognitiva ────────────────────
            TokenType::Component => self.parse_component().map(Declaration::Component),
            TokenType::View => self.parse_view().map(Declaration::View),

            // ── §λ-L-E Fase 13 — Mobile typed channels ──────────
            TokenType::Channel => self.parse_channel().map(Declaration::Channel),

            // ── Tier 3+ structural fallback ─────────────────────
            // Store operations: keyword target { ... } or keyword target ...
            TokenType::Ingest
            | TokenType::Persist
            | TokenType::Retrieve
            | TokenType::Mutate
            | TokenType::Purge
            | TokenType::Transact => self.parse_generic_declaration(),

            // MCP declaration
            TokenType::Mcp => self.parse_generic_declaration(),

            _ => {
                // §Fase 28.e — append "Did you mean X?" hint when the
                // unknown token looks like a typo'd top-level keyword
                // (Levenshtein ≤ 2). D3, D11 ratified 2026-05-10.
                let hint = crate::smart_suggest::suggest_for(
                    &tok.value,
                    crate::smart_suggest::TOP_LEVEL_KEYWORD_NAMES,
                );
                let base = format!(
                    "Unexpected token at top level: '{}' — expected declaration \
                     (persona, context, anchor, flow, run, ...)",
                    tok.value
                );
                let message = if hint.is_empty() {
                    base
                } else {
                    format!("{base}. {hint}")
                };
                Err(ParseError {
                    message,
                    line: tok.line,
                    column: tok.column,
                    ..Default::default()
                })
            }
        }
    }

    // ── IMPORT ───────────────────────────────────────────────────

    fn parse_import(&mut self) -> Result<ImportNode, ParseError> {
        let tok = self.consume(TokenType::Import)?;
        let loc = self.loc_of(&tok);

        let mut path_parts = Vec::new();

        // Optional @ scope
        if self.check(TokenType::At) {
            self.advance();
            let first = self.consume(TokenType::Identifier)?;
            path_parts.push(format!("@{}", first.value));
        } else {
            let first = self.consume(TokenType::Identifier)?;
            path_parts.push(first.value);
        }

        while self.check(TokenType::Dot) {
            self.advance();
            if self.check(TokenType::LBrace) {
                break;
            }
            let part = self.consume(TokenType::Identifier)?;
            path_parts.push(part.value);
        }

        let mut names = Vec::new();
        if self.check(TokenType::LBrace) {
            self.advance();
            names = self.parse_identifier_list()?;
            self.consume(TokenType::RBrace)?;
        }

        // Skip optional APX policy (with apx { ... })
        if self.current().value == "with" {
            self.advance();
            self.advance(); // consume "apx"
            if self.check(TokenType::LBrace) {
                self.skip_braced_block()?;
            }
        }

        Ok(ImportNode {
            module_path: path_parts,
            names,
            loc,
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        })
    }

    // ── PERSONA ──────────────────────────────────────────────────

    fn parse_persona(&mut self) -> Result<PersonaDefinition, ParseError> {
        let tok = self.consume(TokenType::Persona)?;
        let loc = self.loc_of(&tok);
        let name = self.consume(TokenType::Identifier)?.value;
        self.consume(TokenType::LBrace)?;

        let mut node = PersonaDefinition {
            name,
            domain: Vec::new(),
            tone: String::new(),
            confidence_threshold: None,
            cite_sources: None,
            refuse_if: Vec::new(),
            language: String::new(),
            description: String::new(),
            loc,
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };

        while !self.check(TokenType::RBrace) {
            let field_name = self.current().value.clone();
            self.advance();
            self.consume(TokenType::Colon)?;

            match field_name.as_str() {
                "domain" => node.domain = self.parse_string_list()?,
                "tone" => node.tone = self.consume_any_ident_or_kw()?.value,
                "confidence_threshold" => node.confidence_threshold = Some(self.consume_number()?),
                "cite_sources" => node.cite_sources = Some(self.parse_bool()?),
                "refuse_if" => node.refuse_if = self.parse_bracketed_identifiers()?,
                "language" => node.language = self.consume(TokenType::StringLit)?.value,
                "description" => node.description = self.consume(TokenType::StringLit)?.value,
                _ => self.skip_value(),
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    // ── CONTEXT ──────────────────────────────────────────────────

    fn parse_context(&mut self) -> Result<ContextDefinition, ParseError> {
        let tok = self.consume(TokenType::Context)?;
        let loc = self.loc_of(&tok);
        let name = self.consume(TokenType::Identifier)?.value;
        self.consume(TokenType::LBrace)?;

        let mut node = ContextDefinition {
            name,
            memory_scope: String::new(),
            language: String::new(),
            depth: String::new(),
            max_tokens: None,
            temperature: None,
            cite_sources: None,
            loc,
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };

        while !self.check(TokenType::RBrace) {
            let field_name = self.current().value.clone();
            self.advance();
            self.consume(TokenType::Colon)?;

            match field_name.as_str() {
                "memory" => node.memory_scope = self.consume_any_ident_or_kw()?.value,
                "language" => node.language = self.consume(TokenType::StringLit)?.value,
                "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
                "max_tokens" => {
                    node.max_tokens = Some(
                        self.consume(TokenType::Integer)?
                            .value
                            .parse::<i64>()
                            .unwrap_or(0),
                    )
                }
                "temperature" => node.temperature = Some(self.consume_number()?),
                "cite_sources" => node.cite_sources = Some(self.parse_bool()?),
                _ => self.skip_value(),
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    // ── ANCHOR ───────────────────────────────────────────────────

    fn parse_anchor(&mut self) -> Result<AnchorConstraint, ParseError> {
        let tok = self.consume(TokenType::Anchor)?;
        let loc = self.loc_of(&tok);
        let name = self.consume(TokenType::Identifier)?.value;
        self.consume(TokenType::LBrace)?;

        let mut node = AnchorConstraint {
            name,
            require: String::new(),
            reject: Vec::new(),
            enforce: String::new(),
            description: String::new(),
            confidence_floor: None,
            unknown_response: String::new(),
            on_violation: String::new(),
            on_violation_target: String::new(),
            loc,
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };

        while !self.check(TokenType::RBrace) {
            let field_name = self.current().value.clone();
            self.advance();
            self.consume(TokenType::Colon)?;

            match field_name.as_str() {
                "require" => node.require = self.consume_any_ident_or_kw()?.value,
                "description" => node.description = self.consume(TokenType::StringLit)?.value,
                "reject" => node.reject = self.parse_bracketed_identifiers()?,
                "enforce" => node.enforce = self.consume_any_ident_or_kw()?.value,
                "confidence_floor" => node.confidence_floor = Some(self.consume_number()?),
                "unknown_response" => {
                    node.unknown_response = self.consume(TokenType::StringLit)?.value
                }
                "on_violation" => {
                    // Parse: raise ErrorName | fallback(...) | identifier
                    let action = self.consume_any_ident_or_kw()?.value;
                    node.on_violation = action.clone();
                    if action == "raise" || action == "fallback" {
                        node.on_violation_target = self.consume_any_ident_or_kw()?.value;
                    }
                }
                _ => self.skip_value(),
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    // ── MEMORY ───────────────────────────────────────────────────

    fn parse_memory(&mut self) -> Result<MemoryDefinition, ParseError> {
        let tok = self.consume(TokenType::Memory)?;
        let loc = self.loc_of(&tok);
        let name = self.consume(TokenType::Identifier)?.value;
        self.consume(TokenType::LBrace)?;

        let mut node = MemoryDefinition {
            name,
            store: String::new(),
            backend: String::new(),
            retrieval: String::new(),
            decay: String::new(),
            loc,
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };

        while !self.check(TokenType::RBrace) {
            let field_name = self.current().value.clone();
            self.advance();
            self.consume(TokenType::Colon)?;

            match field_name.as_str() {
                "store" => node.store = self.consume_any_ident_or_kw()?.value,
                "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
                "retrieval" => node.retrieval = self.consume_any_ident_or_kw()?.value,
                "decay" => {
                    if self.check(TokenType::Duration) {
                        node.decay = self.advance().value.clone();
                    } else {
                        node.decay = self.consume_any_ident_or_kw()?.value;
                    }
                }
                _ => self.skip_value(),
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    // ── TOOL ─────────────────────────────────────────────────────

    fn parse_tool(&mut self) -> Result<ToolDefinition, ParseError> {
        let tok = self.consume(TokenType::Tool)?;
        let loc = self.loc_of(&tok);
        let name = self.consume(TokenType::Identifier)?.value;
        self.consume(TokenType::LBrace)?;

        let mut node = ToolDefinition {
            name,
            provider: String::new(),
            max_results: None,
            filter_expr: String::new(),
            timeout: String::new(),
            runtime: String::new(),
            sandbox: None,
            effects: None,
            loc,
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };

        while !self.check(TokenType::RBrace) {
            let field_name = self.current().value.clone();
            self.advance();
            self.consume(TokenType::Colon)?;

            match field_name.as_str() {
                "provider" => node.provider = self.consume_any_ident_or_kw()?.value,
                "max_results" => {
                    node.max_results = Some(
                        self.consume(TokenType::Integer)?
                            .value
                            .parse::<i64>()
                            .unwrap_or(0),
                    )
                }
                "filter" => node.filter_expr = self.parse_filter_expression()?,
                "timeout" => node.timeout = self.consume(TokenType::Duration)?.value,
                "runtime" => node.runtime = self.consume_any_ident_or_kw()?.value,
                "sandbox" => node.sandbox = Some(self.parse_bool()?),
                "effects" => node.effects = Some(self.parse_effect_row()?),
                _ => self.skip_value(),
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    fn parse_filter_expression(&mut self) -> Result<String, ParseError> {
        let name = self.consume_any_ident_or_kw()?.value;
        if self.check(TokenType::LParen) {
            self.advance();
            let mut parts = vec![name, "(".to_string()];
            while !self.check(TokenType::RParen) {
                parts.push(self.advance().value.clone());
            }
            self.consume(TokenType::RParen)?;
            parts.push(")".to_string());
            Ok(parts.join(""))
        } else {
            Ok(name)
        }
    }

    fn parse_effect_row(&mut self) -> Result<EffectRow, ParseError> {
        let tok = self.consume(TokenType::Lt)?;
        let loc = self.loc_of(&tok);
        let mut effects = Vec::new();
        let mut epistemic_level = String::new();

        while !self.check(TokenType::Gt) {
            let name = self.consume_any_ident_or_kw()?.value;
            if self.check(TokenType::Colon) {
                self.advance();
                // Fase 11.c / 11.e — qualifiers can be compound slugs
                // from a closed catalogue:
                //
                //   * dot-separated  — `legal:HIPAA.164_502`,
                //                       `legal:GDPR.Art6.Consent`,
                //                       `legal:PCI_DSS.v4_Req3`
                //   * colon-separated — `ots:transform:mulaw8:pcm16`,
                //                       `ots:backend:native`
                //   * mixed           — supported by the same loop.
                //
                // The lexer fragments dotted slugs across IDENT /
                // INTEGER tokens (e.g., `164_502` lexes as INTEGER
                // `164` + IDENT `_502` because `_` starts a fresh
                // identifier); we recombine here using source-column
                // adjacency so the type checker sees the catalog
                // string verbatim.
                let level = self.parse_qualifier_value()?;
                if name == "epistemic" {
                    epistemic_level = level;
                } else {
                    effects.push(format!("{name}:{level}"));
                }
            } else {
                effects.push(name);
            }
            if self.check(TokenType::Comma) {
                self.advance();
            }
        }
        self.consume(TokenType::Gt)?;

        Ok(EffectRow {
            effects,
            epistemic_level,
            loc,
        })
    }

    /// Parse a compound qualifier value following an effect's first
    /// colon — supports both dot-separated (`HIPAA.164_502`) and
    /// colon-separated (`transform:mulaw8:pcm16`) catalogue slugs, as
    /// well as mixed forms.
    ///
    /// The grammar is: `segment ((`.` | `:`) segment)*` where a
    /// segment is a contiguous run of IDENT / INTEGER tokens (see
    /// [`Self::consume_dotted_slug_segment`]).
    fn parse_qualifier_value(&mut self) -> Result<String, ParseError> {
        let mut buf = self.consume_dotted_slug_segment()?;
        loop {
            let sep = if self.check(TokenType::Dot) {
                '.'
            } else if self.check(TokenType::Colon) {
                ':'
            } else {
                break;
            };
            self.advance();
            let part = self.consume_dotted_slug_segment()?;
            buf.push(sep);
            buf.push_str(&part);
        }
        Ok(buf)
    }

    /// Consume a contiguous run of IDENT / INTEGER / keyword-ident
    /// tokens whose source positions are adjacent (no whitespace
    /// between them), concatenating their text into a single segment.
    ///
    /// Needed for closed-catalogue qualifier slugs whose segment
    /// mixes digits and identifier characters — e.g. `HIPAA.164_502`
    /// lexes as INTEGER `164` + IDENT `_502` because `_` starts a
    /// fresh identifier; the catalog value is the concatenation
    /// `164_502`. Adjacency is determined by matching
    /// `(line, column + len)` of the previous token against the next
    /// token's start position.
    fn consume_dotted_slug_segment(&mut self) -> Result<String, ParseError> {
        let first = self.consume_any_ident_or_kw()?;
        let mut buf = first.value.clone();
        let mut next_line = first.line;
        let mut next_col = first.column + first.value.chars().count() as u32;
        loop {
            let cur = self.current();
            let is_segment_token = matches!(cur.ttype, TokenType::Identifier | TokenType::Integer,);
            if !is_segment_token {
                break;
            }
            if cur.line != next_line || cur.column != next_col {
                break;
            }
            buf.push_str(&cur.value);
            next_col = cur.column + cur.value.chars().count() as u32;
            next_line = cur.line;
            self.pos += 1;
        }
        Ok(buf)
    }

    // ── TYPE ─────────────────────────────────────────────────────

    fn parse_type_def(&mut self) -> Result<TypeDefinition, ParseError> {
        let tok = self.consume(TokenType::Type)?;
        let loc = self.loc_of(&tok);
        let name = self.consume(TokenType::Identifier)?.value;

        let mut node = TypeDefinition {
            name,
            fields: Vec::new(),
            range_constraint: None,
            where_clause: None,
            compliance: Vec::new(),
            loc: loc.clone(),
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };

        // Optional range: (0.0..1.0)
        if self.check(TokenType::LParen) {
            self.advance();
            let min_val = self.consume_number()?;
            self.consume(TokenType::DotDot)?;
            let max_val = self.consume_number()?;
            self.consume(TokenType::RParen)?;
            node.range_constraint = Some(RangeConstraint {
                min_value: min_val,
                max_value: max_val,
                loc: loc.clone(),
            });
        }

        // Optional where clause
        if self.check(TokenType::Where) {
            self.advance();
            let mut expr_parts = Vec::new();
            while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
                if self.check(TokenType::Eof) {
                    break;
                }
                expr_parts.push(self.advance().value.clone());
            }
            node.where_clause = Some(WhereClause {
                expression: expr_parts.join(" "),
                loc: loc.clone(),
            });
        }

        // Optional ESK Fase 6.1 — `compliance [HIPAA, ...]` prefix modifier
        // between `type Name` / `range` / `where` and the body `{`.
        if self.check(TokenType::Identifier) && self.current().value == "compliance" {
            self.advance();
            node.compliance = self.parse_bracketed_identifiers()?;
        }

        // Optional body: { field: Type, ... }
        if self.check(TokenType::LBrace) {
            self.advance();
            while !self.check(TokenType::RBrace) {
                let field_name = self.consume(TokenType::Identifier)?;
                let field_loc = self.loc_of(&field_name);
                self.consume(TokenType::Colon)?;
                let type_expr = self.parse_type_expr()?;
                node.fields.push(TypeField {
                    name: field_name.value,
                    type_expr,
                    loc: field_loc,
                });
                if self.check(TokenType::Comma) {
                    self.advance();
                }
            }
            self.consume(TokenType::RBrace)?;
        }

        Ok(node)
    }

    fn parse_type_expr(&mut self) -> Result<TypeExpr, ParseError> {
        let name_tok = self.consume(TokenType::Identifier)?;
        let loc = self.loc_of(&name_tok);
        let mut generic_param = String::new();
        let mut optional = false;

        if self.check(TokenType::Lt) {
            self.advance();
            // §Fase 39.a — recursive: the generic param can itself be a
            // nested type expression. `FlowEnvelope<List<TenantRecord>>`
            // parses as outer=FlowEnvelope, inner=List<TenantRecord>.
            // Pre-39.a the inner had to be a single Identifier; nested
            // generics like the canonical FlowEnvelope<T> wrapper
            // required this lift. Backwards-compat preserved for
            // single-level generics like `Stream<Token>` and
            // `List<T>` — the recursion lands once and returns the
            // same flat string the v1.x parser produced.
            let inner = self.parse_type_expr()?;
            generic_param = if inner.generic_param.is_empty() {
                inner.name
            } else {
                format!("{}<{}>", inner.name, inner.generic_param)
            };
            self.consume(TokenType::Gt)?;
        }
        if self.check(TokenType::Question) {
            self.advance();
            optional = true;
        }

        Ok(TypeExpr {
            name: name_tok.value,
            generic_param,
            optional,
            loc,
        })
    }

    /// Parse a type expression in a context where the AST stores the
    /// shape as a flat string (step / reason / forge / ots-apply
    /// productions). Mirrors Python `_parse_output_type_string`.
    ///
    /// Accepts:
    /// - `Identifier`        → `"Identifier"`
    /// - `Stream<String>`    → `"Stream<String>"`
    /// - `Optional?`         → `"Optional?"`
    /// - `Stream<String>?`   → `"Stream<String>?"`
    ///
    /// **Why this exists** — pre-fix, the step parser called
    /// `consume(TokenType::Identifier)?.value` which captured only
    /// the head identifier and left `< … >` unconsumed. For
    /// `output: Stream<Token>`, this produced `output_type =
    /// "Stream"`, and downstream `flow_has_stream_output`'s
    /// `starts_with("Stream<") && ends_with('>')` predicate then
    /// returned false → `implicit_transport == "json"` → the
    /// dynamic-route fallback in `axon-rs` served JSON instead of
    /// SSE even when the adopter's source canonically declared the
    /// algebraic stream effect. Surfaced 2026-05-12 by adopter
    /// `docs/MIGRATION_TO_AXON.md` audit after the v1.23.0 wire-
    /// layer didn't honor the declarative effect. Python parser was
    /// fixed for the same gap 2026-05-09; this is the Rust cross-
    /// stack catch-up.
    fn parse_output_type_string(&mut self) -> Result<String, ParseError> {
        let expr = self.parse_type_expr()?;
        let mut s = expr.name;
        if !expr.generic_param.is_empty() {
            s.push('<');
            s.push_str(&expr.generic_param);
            s.push('>');
        }
        if expr.optional {
            s.push('?');
        }
        Ok(s)
    }

    // ── FLOW ─────────────────────────────────────────────────────

    fn parse_flow(&mut self) -> Result<FlowDefinition, ParseError> {
        let tok = self.consume(TokenType::Flow)?;
        let loc = self.loc_of(&tok);
        let name = self.consume(TokenType::Identifier)?.value;

        self.consume(TokenType::LParen)?;
        let mut parameters = Vec::new();
        if !self.check(TokenType::RParen) {
            parameters = self.parse_param_list()?;
        }
        self.consume(TokenType::RParen)?;

        let mut return_type = None;
        if self.check(TokenType::Arrow) {
            self.advance();
            return_type = Some(self.parse_type_expr()?);
        }

        self.consume(TokenType::LBrace)?;
        let mut body = Vec::new();
        while !self.check(TokenType::RBrace) {
            body.push(self.parse_flow_step()?);
        }
        self.consume(TokenType::RBrace)?;

        Ok(FlowDefinition {
            name,
            parameters,
            return_type,
            body,
            loc,
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        })
    }

    fn parse_param_list(&mut self) -> Result<Vec<Parameter>, ParseError> {
        let mut params = Vec::new();

        let name = self.consume(TokenType::Identifier)?;
        let ploc = self.loc_of(&name);
        self.consume(TokenType::Colon)?;
        let type_expr = self.parse_type_expr()?;
        params.push(Parameter {
            name: name.value,
            type_expr,
            loc: ploc,
        });

        while self.check(TokenType::Comma) {
            self.advance();
            let name = self.consume(TokenType::Identifier)?;
            let ploc = self.loc_of(&name);
            self.consume(TokenType::Colon)?;
            let type_expr = self.parse_type_expr()?;
            params.push(Parameter {
                name: name.value,
                type_expr,
                loc: ploc,
            });
        }
        Ok(params)
    }

    // ── FLOW STEP dispatch ───────────────────────────────────────

    fn parse_flow_step(&mut self) -> Result<FlowStep, ParseError> {
        let tok = self.current().clone();

        match tok.ttype {
            TokenType::Step => self.parse_step().map(FlowStep::Step),
            TokenType::If => self.parse_if().map(FlowStep::If),
            TokenType::For => self.parse_for_in().map(FlowStep::ForIn),
            TokenType::Let => self.parse_let().map(FlowStep::Let),
            TokenType::Return => self.parse_return().map(FlowStep::Return),
            TokenType::Break => self.parse_break().map(FlowStep::Break),
            TokenType::Continue => self.parse_continue().map(FlowStep::Continue),
            TokenType::Lambda => self.parse_lambda_data_apply().map(FlowStep::LambdaDataApply),

            // ── Tier 2 flow steps (typed AST) ─────────────────────
            TokenType::Probe => self.parse_flow_step_simple("probe").map(|l| FlowStep::Probe(ProbeStep { target: l.1, loc: l.0 })),
            TokenType::Reason => self.parse_flow_step_simple("reason").map(|l| FlowStep::Reason(ReasonStep { strategy: String::new(), target: l.1, loc: l.0 })),
            TokenType::Validate => self.parse_flow_step_simple("validate").map(|l| FlowStep::Validate(ValidateStep { target: l.1, rule: String::new(), loc: l.0 })),
            TokenType::Refine => self.parse_flow_step_simple("refine").map(|l| FlowStep::Refine(RefineStep { target: l.1, strategy: String::new(), loc: l.0 })),
            TokenType::Weave => self.parse_weave_step(),
            TokenType::Use => self.parse_use_step(),
            TokenType::Remember => self.parse_remember_step(),
            TokenType::Recall => self.parse_recall_step(),
            TokenType::Par => self.parse_block_step("par").map(|l| FlowStep::Par(ParBlock { loc: l })),
            TokenType::Hibernate => self.parse_hibernate_step(),
            TokenType::Deliberate => self.parse_block_step("deliberate").map(|l| FlowStep::Deliberate(DeliberateBlock { loc: l })),
            TokenType::Consensus => self.parse_block_step("consensus").map(|l| FlowStep::Consensus(ConsensusBlock { loc: l })),
            TokenType::Forge => self.parse_block_step("forge").map(|l| FlowStep::Forge(ForgeBlock { loc: l })),
            TokenType::Focus => self.parse_flow_step_simple("focus").map(|l| FlowStep::Focus(FocusStep { expression: l.1, loc: l.0 })),
            TokenType::Associate => self.parse_associate_step(),
            TokenType::Aggregate => self.parse_aggregate_step(),
            TokenType::Explore => self.parse_explore_step(),
            TokenType::Ingest => self.parse_ingest_step(),
            TokenType::Shield => self.parse_apply_step("shield").map(|l| FlowStep::ShieldApply(ShieldApplyStep { shield_name: l.1, target: l.2, output_type: l.3, loc: l.0 })),
            TokenType::Stream => self.parse_block_step("stream").map(|l| FlowStep::Stream(StreamBlock { loc: l })),
            TokenType::Navigate => self.parse_navigate_step(),
            TokenType::Drill => self.parse_drill_step(),
            TokenType::Trail => self.parse_flow_step_simple("trail").map(|l| FlowStep::Trail(TrailStep { navigate_ref: l.1, loc: l.0 })),
            TokenType::Corroborate => self.parse_corroborate_step(),
            TokenType::Ots => self.parse_apply_step("ots").map(|l| FlowStep::OtsApply(OtsApplyStep { ots_name: l.1, target: l.2, output_type: l.3, loc: l.0 })),
            TokenType::Mandate => self.parse_apply_step("mandate").map(|l| FlowStep::MandateApply(MandateApplyStep { mandate_name: l.1, target: l.2, output_type: l.3, loc: l.0 })),
            TokenType::Compute => self.parse_apply_step("compute").map(|l| FlowStep::ComputeApply(ComputeApplyStep { compute_name: l.1, arguments: Vec::new(), output_name: l.3, loc: l.0 })),
            TokenType::Listen => self.parse_listen_step(),
            TokenType::Daemon => self.parse_flow_step_simple("daemon").map(|l| FlowStep::DaemonStep(DaemonStepNode { daemon_ref: l.1, loc: l.0 })),
            // §λ-L-E Fase 13 — Mobile typed channels (paper §3.1, §3.2, §4.3)
            TokenType::Emit => self.parse_emit_step(),
            TokenType::Publish => self.parse_publish_step(),
            TokenType::Discover => self.parse_discover_step(),
            TokenType::Persist => self.parse_persist_step(),
            TokenType::Retrieve => self.parse_retrieve_step(),
            TokenType::Mutate => self.parse_mutate_step(),
            TokenType::Purge => self.parse_store_where_step().map(|(loc, store_name, where_expr)| FlowStep::Purge(PurgeStep { store_name, where_expr, loc })),
            TokenType::Transact => self.parse_block_step("transact").map(|l| FlowStep::Transact(TransactBlock { loc: l })),

            _ => {
                // §Fase 28.e — append "Did you mean X?" hint when the
                // unknown token looks like a typo'd flow-body keyword
                // (e.g. `stepp` / `reasn` / `validte`). D3, D11.
                let hint = crate::smart_suggest::suggest_for(
                    &tok.value,
                    crate::smart_suggest::FLOW_BODY_KEYWORD_NAMES,
                );
                let base = format!(
                    "Unexpected token in flow body: '{}' — expected step, if, for, let, return, ...",
                    tok.value
                );
                let message = if hint.is_empty() {
                    base
                } else {
                    format!("{base}. {hint}")
                };
                Err(ParseError {
                    message,
                    line: tok.line,
                    column: tok.column,
                    ..Default::default()
                })
            }
        }
    }

    // ── STEP ─────────────────────────────────────────────────────

    fn parse_step(&mut self) -> Result<StepNode, ParseError> {
        let tok = self.consume(TokenType::Step)?;
        let loc = self.loc_of(&tok);
        let name = self.consume(TokenType::Identifier)?.value;

        let mut persona_ref = String::new();
        if self.check(TokenType::Use) {
            self.advance();
            persona_ref = self.consume_any_ident_or_kw()?.value;
        }

        self.consume(TokenType::LBrace)?;

        let mut node = StepNode {
            name,
            persona_ref,
            given: String::new(),
            ask: String::new(),
            output_type: String::new(),
            confidence_floor: None,
            navigate_ref: String::new(),
            apply_ref: String::new(),
            loc,
        };

        while !self.check(TokenType::RBrace) {
            let inner = self.current().clone();

            match inner.ttype {
                TokenType::Given => {
                    self.advance();
                    self.consume(TokenType::Colon)?;
                    node.given = self.parse_expression_string()?;
                }
                TokenType::Ask => {
                    self.advance();
                    self.consume(TokenType::Colon)?;
                    node.ask = self.consume(TokenType::StringLit)?.value;
                }
                TokenType::Output => {
                    // Mirror of Python `_parse_step` `case "output":`
                    // which uses `_parse_output_type_string` — accepts
                    // the FULL generic-aware shape `Stream<T>`,
                    // `Stream<T>?`, `Identifier?`, NOT just the bare
                    // head identifier. Pre-fix the step parser dropped
                    // `<T>` and downstream `flow_has_stream_output`'s
                    // `starts_with("Stream<") && ends_with('>')` then
                    // returned false → `implicit_transport == "json"`
                    // → dynamic routes served JSON instead of SSE.
                    self.advance();
                    self.consume(TokenType::Colon)?;
                    node.output_type = self.parse_output_type_string()?;
                }
                TokenType::Navigate => {
                    self.advance();
                    self.consume(TokenType::Colon)?;
                    node.navigate_ref = self.parse_dotted_identifier()?;
                }
                TokenType::Identifier if inner.value == "confidence_floor" => {
                    self.advance();
                    self.consume(TokenType::Colon)?;
                    node.confidence_floor = Some(self.consume_number()?);
                }
                TokenType::Identifier if inner.value == "apply" => {
                    self.advance();
                    self.consume(TokenType::Colon)?;
                    node.apply_ref = self.consume_any_ident_or_kw()?.value;
                }
                // Sub-constructs (use, probe, reason, weave, stream) → skip structurally
                TokenType::Use
                | TokenType::Probe
                | TokenType::Reason
                | TokenType::Weave
                | TokenType::Stream => {
                    self.skip_flow_step_structural()?;
                }
                _ => {
                    return Err(ParseError {
                        message: format!(
                            "Unexpected token in step body: '{}' — expected given, ask, use, \
                             probe, reason, weave, stream, output, confidence_floor, navigate, apply",
                            inner.value
                        ),
                        line: inner.line,
                        column: inner.column,
                                            ..Default::default()
                    });
                }
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    /// Skip a flow-level sub-construct structurally (consume keyword + args + optional block).
    fn skip_flow_step_structural(&mut self) -> Result<(), ParseError> {
        // Consume the keyword
        self.advance();
        // Consume tokens until we hit a { or a closing }, or a known flow step keyword
        while !self.check(TokenType::LBrace)
            && !self.check(TokenType::RBrace)
            && !self.check(TokenType::Eof)
        {
            // Check if we hit a new step-level keyword (means this was a one-liner)
            let tt = &self.current().ttype;
            if matches!(
                tt,
                TokenType::Step
                    | TokenType::Given
                    | TokenType::Ask
                    | TokenType::Output
                    | TokenType::Navigate
                    | TokenType::Use
                    | TokenType::Probe
                    | TokenType::Reason
                    | TokenType::Weave
                    | TokenType::Stream
                    | TokenType::If
                    | TokenType::For
                    | TokenType::Let
                    | TokenType::Return
            ) {
                return Ok(());
            }
            self.advance();
        }
        // If block, skip it
        if self.check(TokenType::LBrace) {
            self.skip_braced_block()?;
        }
        Ok(())
    }

    // ── INTENT ───────────────────────────────────────────────────

    fn parse_intent(&mut self) -> Result<IntentNode, ParseError> {
        let tok = self.consume(TokenType::Intent)?;
        let loc = self.loc_of(&tok);
        let name = self.consume(TokenType::Identifier)?.value;
        self.consume(TokenType::LBrace)?;

        let mut node = IntentNode {
            name,
            given: String::new(),
            ask: String::new(),
            output_type: None,
            confidence_floor: None,
            loc,
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };

        while !self.check(TokenType::RBrace) {
            let field_name = self.current().value.clone();
            self.advance();
            self.consume(TokenType::Colon)?;

            match field_name.as_str() {
                "given" => node.given = self.consume(TokenType::Identifier)?.value,
                "ask" => node.ask = self.consume(TokenType::StringLit)?.value,
                "output" => node.output_type = Some(self.parse_type_expr()?),
                "confidence_floor" => node.confidence_floor = Some(self.consume_number()?),
                _ => self.skip_value(),
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    // ── RUN ──────────────────────────────────────────────────────

    fn parse_run(&mut self) -> Result<RunStatement, ParseError> {
        let tok = self.consume(TokenType::Run)?;
        let loc = self.loc_of(&tok);
        let flow_name = self.consume(TokenType::Identifier)?.value;

        self.consume(TokenType::LParen)?;
        let mut arguments = Vec::new();
        if !self.check(TokenType::RParen) {
            arguments = self.parse_argument_list()?;
        }
        self.consume(TokenType::RParen)?;

        let mut node = RunStatement {
            flow_name,
            arguments,
            persona: String::new(),
            context: String::new(),
            anchors: Vec::new(),
            on_failure: String::new(),
            on_failure_params: Vec::new(),
            output_to: String::new(),
            effort: String::new(),
            loc,
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };

        while self.check_run_modifier() {
            let mod_tok = self.current().clone();
            match mod_tok.ttype {
                TokenType::As => {
                    self.advance();
                    node.persona = self.consume(TokenType::Identifier)?.value;
                }
                TokenType::Within => {
                    self.advance();
                    node.context = self.consume(TokenType::Identifier)?.value;
                }
                TokenType::ConstrainedBy => {
                    self.advance();
                    node.anchors = self.parse_bracketed_identifiers()?;
                }
                TokenType::OnFailure => {
                    self.advance();
                    self.consume(TokenType::Colon)?;
                    node.on_failure = self.consume_any_ident_or_kw()?.value;
                    // Parse optional params: (key: val, ...)
                    if self.check(TokenType::LParen) {
                        self.advance();
                        while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
                            let key = self.consume_any_ident_or_kw()?.value;
                            self.consume(TokenType::Colon)?;
                            let val = self.consume_any_ident_or_kw()?.value;
                            node.on_failure_params.push((key, val));
                            if self.check(TokenType::Comma) {
                                self.advance();
                            }
                        }
                        if self.check(TokenType::RParen) {
                            self.advance();
                        }
                    }
                }
                TokenType::OutputTo => {
                    self.advance();
                    self.consume(TokenType::Colon)?;
                    node.output_to = self.consume(TokenType::StringLit)?.value;
                }
                TokenType::Effort => {
                    self.advance();
                    self.consume(TokenType::Colon)?;
                    node.effort = self.consume_any_ident_or_kw()?.value;
                }
                _ => break,
            }
        }

        Ok(node)
    }

    // ── EPISTEMIC BLOCK ──────────────────────────────────────────

    fn parse_epistemic_block(&mut self) -> Result<EpistemicBlock, ParseError> {
        let tok = self.current().clone();
        let mode = match tok.ttype {
            TokenType::Know => "know",
            TokenType::Believe => "believe",
            TokenType::Speculate => "speculate",
            TokenType::Doubt => "doubt",
            _ => unreachable!(),
        };
        self.advance();
        let loc = self.loc_of(&tok);

        self.consume(TokenType::LBrace)?;
        let mut body = Vec::new();
        while !self.check(TokenType::RBrace) {
            body.push(self.parse_declaration()?);
        }
        self.consume(TokenType::RBrace)?;

        Ok(EpistemicBlock {
            mode: mode.to_string(),
            body,
            loc,
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        })
    }

    // ── IF ────────────────────────────────────────────────────────

    fn parse_if(&mut self) -> Result<ConditionalNode, ParseError> {
        let tok = self.consume(TokenType::If)?;
        let loc = self.loc_of(&tok);

        // Parse condition
        let mut parts = vec![self.consume_any_ident_or_kw()?.value];
        while self.check(TokenType::Dot) {
            self.advance();
            parts.push(self.consume_any_ident_or_kw()?.value);
        }
        let condition = parts.join(".");

        let mut comparison_op = String::new();
        let mut comparison_value = String::new();
        if self.check_comparison() {
            comparison_op = self.advance().value.clone();
            let val_tok = self.current().clone();
            if val_tok.ttype == TokenType::StringLit {
                comparison_value = val_tok.value;
                self.advance();
            } else {
                comparison_value = self.advance().value.clone();
            }
        }

        // Compound conditions (or)
        let mut conditions = Vec::new();
        let mut conjunctor = String::new();
        while self.check(TokenType::Or) {
            conjunctor = "or".to_string();
            self.advance();
            let mut cond_parts = vec![self.consume_any_ident_or_kw()?.value];
            while self.check(TokenType::Dot) {
                self.advance();
                cond_parts.push(self.consume_any_ident_or_kw()?.value);
            }
            let cond_str = cond_parts.join(".");
            let mut cond_op = String::new();
            let mut cond_val = String::new();
            if self.check_comparison() {
                cond_op = self.advance().value.clone();
                let val_tok = self.current().clone();
                if val_tok.ttype == TokenType::StringLit {
                    cond_val = val_tok.value;
                    self.advance();
                } else {
                    cond_val = self.advance().value.clone();
                }
            }
            conditions.push((cond_str, cond_op, cond_val));
        }

        let mut then_body = Vec::new();
        let mut else_body = Vec::new();

        // Arrow form or block form
        if self.check(TokenType::Arrow) {
            self.advance();
            then_body.push(self.parse_flow_step()?);
        } else if self.check(TokenType::LBrace) {
            self.advance();
            while !self.check(TokenType::RBrace) {
                then_body.push(self.parse_flow_step()?);
            }
            self.consume(TokenType::RBrace)?;
        }

        // Else branch
        if self.check(TokenType::Else) {
            self.advance();
            if self.check(TokenType::Arrow) {
                self.advance();
                else_body.push(self.parse_flow_step()?);
            } else if self.check(TokenType::LBrace) {
                self.advance();
                while !self.check(TokenType::RBrace) {
                    else_body.push(self.parse_flow_step()?);
                }
                self.consume(TokenType::RBrace)?;
            }
        }

        Ok(ConditionalNode {
            condition,
            comparison_op,
            comparison_value,
            then_body,
            else_body,
            conditions,
            conjunctor,
            loc,
        })
    }

    // ── FOR IN ───────────────────────────────────────────────────

    fn parse_for_in(&mut self) -> Result<ForInStatement, ParseError> {
        let tok = self.consume(TokenType::For)?;
        let loc = self.loc_of(&tok);
        let variable = self.consume(TokenType::Identifier)?.value;
        self.consume(TokenType::In)?;
        let iterable = self.parse_dotted_identifier()?;

        self.consume(TokenType::LBrace)?;
        // Fase 19.e — increment loop_depth so `parse_break` /
        // `parse_continue` inside the body pass the scope check.
        // Decrement on every exit path (Ok / Err) so a parse error
        // mid-body does not leave the depth permanently elevated
        // for later top-level parsing — `?` would skip the
        // decrement otherwise.
        self.loop_depth += 1;
        let body_result = (|| -> Result<Vec<FlowStep>, ParseError> {
            let mut body = Vec::new();
            while !self.check(TokenType::RBrace) {
                body.push(self.parse_flow_step()?);
            }
            Ok(body)
        })();
        self.loop_depth -= 1;
        let body = body_result?;
        self.consume(TokenType::RBrace)?;

        Ok(ForInStatement {
            variable,
            iterable,
            body,
            loc,
        })
    }

    /// Fase 19.e — `break` keyword. Compile-time scope check
    /// (`loop_depth == 0`) rejects break outside a for-in body.
    fn parse_break(&mut self) -> Result<BreakStatement, ParseError> {
        let tok = self.consume(TokenType::Break)?;
        let loc = self.loc_of(&tok);
        if self.loop_depth == 0 {
            return Err(ParseError {
                message: "'break' outside of a for-in loop body".to_string(),
                line: tok.line,
                column: tok.column,
                            ..Default::default()
            });
        }
        Ok(BreakStatement { loc })
    }

    /// Fase 19.e — `continue` keyword. Same scope check as
    /// `parse_break`.
    fn parse_continue(&mut self) -> Result<ContinueStatement, ParseError> {
        let tok = self.consume(TokenType::Continue)?;
        let loc = self.loc_of(&tok);
        if self.loop_depth == 0 {
            return Err(ParseError {
                message: "'continue' outside of a for-in loop body".to_string(),
                line: tok.line,
                column: tok.column,
                            ..Default::default()
            });
        }
        Ok(ContinueStatement { loc })
    }

    // ── LET ──────────────────────────────────────────────────────

    fn parse_let(&mut self) -> Result<LetStatement, ParseError> {
        let tok = self.consume(TokenType::Let)?;
        let loc = self.loc_of(&tok);

        // Name can be an identifier or a keyword used as binding name
        let name = self.consume_any_ident_or_kw()?.value;
        self.consume(TokenType::Assign)?;
        // Fase 17.a — reset side-channel before parsing value; the
        // atom / expr helpers tag the kind as they descend.
        self.last_let_value_kind = "literal".to_string();
        let value = self.parse_let_value_expr()?;

        Ok(LetStatement {
            identifier: name,
            value_expr: value,
            value_kind: self.last_let_value_kind.clone(),
            loc,
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        })
    }

    fn parse_let_value_expr(&mut self) -> Result<String, ParseError> {
        let atom = self.parse_let_atom()?;

        // Arithmetic expression: collect as string
        if matches!(
            self.current().ttype,
            TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
        ) {
            let mut parts = vec![atom];
            while matches!(
                self.current().ttype,
                TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
            ) {
                parts.push(self.advance().value.clone());
                parts.push(self.parse_let_atom()?);
            }
            self.last_let_value_kind = "expression".to_string();
            return Ok(parts.join(" "));
        }
        Ok(atom)
    }

    fn parse_let_atom(&mut self) -> Result<String, ParseError> {
        let tok = self.current().clone();

        match tok.ttype {
            TokenType::StringLit => {
                self.last_let_value_kind = "literal".to_string();
                self.advance();
                Ok(tok.value)
            }
            TokenType::Integer | TokenType::Float => {
                self.last_let_value_kind = "literal".to_string();
                self.advance();
                Ok(tok.value)
            }
            TokenType::Bool => {
                self.last_let_value_kind = "literal".to_string();
                self.advance();
                Ok(tok.value)
            }
            TokenType::Identifier => {
                self.last_let_value_kind = "reference".to_string();
                self.parse_dotted_identifier()
            }
            TokenType::LBracket => {
                self.last_let_value_kind = "literal".to_string();
                self.parse_let_list_literal()
            }
            _ => {
                // Keywords starting a dotted path (pix.document_tree)
                if self.pos + 1 < self.tokens.len()
                    && self.tokens[self.pos + 1].ttype == TokenType::Dot
                {
                    self.last_let_value_kind = "reference".to_string();
                    return self.parse_dotted_identifier();
                }
                Err(ParseError {
                    message: format!(
                        "Expected value expression, found {:?}('{}')",
                        tok.ttype, tok.value
                    ),
                    line: tok.line,
                    column: tok.column,
                                    ..Default::default()
                })
            }
        }
    }

    fn parse_let_list_literal(&mut self) -> Result<String, ParseError> {
        self.consume(TokenType::LBracket)?;
        let mut items = Vec::new();
        if !self.check(TokenType::RBracket) {
            items.push(self.parse_let_value_expr()?);
            while self.check(TokenType::Comma) {
                self.advance();
                if self.check(TokenType::RBracket) {
                    break; // trailing comma
                }
                items.push(self.parse_let_value_expr()?);
            }
        }
        self.consume(TokenType::RBracket)?;
        Ok(format!("[{}]", items.join(", ")))
    }

    // ── RETURN ───────────────────────────────────────────────────

    fn parse_return(&mut self) -> Result<ReturnStatement, ParseError> {
        let tok = self.consume(TokenType::Return)?;
        let loc = self.loc_of(&tok);
        let value = self.parse_let_value_expr()?;
        Ok(ReturnStatement {
            value_expr: value,
            loc,
        })
    }

    // ── TIER 2 FLOW STEP HELPERS ────────────────────────────────────

    /// Parse: keyword target (consumes keyword + one identifier/keyword-as-value).
    fn parse_flow_step_simple(&mut self, _kw: &str) -> Result<(Loc, String), ParseError> {
        let tok = self.current().clone();
        self.advance(); // consume keyword
        let target = if self.at_declaration_start()
            || self.check(TokenType::RBrace)
            || self.check(TokenType::Eof)
        {
            String::new()
        } else {
            self.consume_any_ident_or_kw()?.value.clone()
        };
        // Skip optional braced block
        if self.check(TokenType::LBrace) {
            self.skip_braced_block()?;
        }
        Ok((
            Loc {
                line: tok.line,
                column: tok.column,
            },
            target,
        ))
    }

    /// Parse: keyword { ... } — block-level step, skip body structurally.
    fn parse_block_step(&mut self, _kw: &str) -> Result<Loc, ParseError> {
        let tok = self.current().clone();
        self.advance();
        // Skip optional arguments before brace
        while !self.check(TokenType::LBrace)
            && !self.check(TokenType::RBrace)
            && !self.check(TokenType::Eof)
            && !self.at_declaration_start()
        {
            self.advance();
        }
        if self.check(TokenType::LBrace) {
            self.skip_braced_block()?;
        }
        Ok(Loc {
            line: tok.line,
            column: tok.column,
        })
    }

    /// Parse: keyword Name on target -> output_type (apply pattern).
    fn parse_apply_step(&mut self, _kw: &str) -> Result<(Loc, String, String, String), ParseError> {
        let tok = self.current().clone();
        self.advance(); // consume keyword
        let name = self.consume_any_ident_or_kw()?.value.clone();
        let mut target = String::new();
        let mut output_type = String::new();
        // "on" target
        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
            let next = self.current().clone();
            if next.value == "on" {
                self.advance();
                target = self.consume_any_ident_or_kw()?.value.clone();
            }
        }
        // -> output_type
        if self.check(TokenType::Arrow) {
            self.advance();
            output_type = self.consume_any_ident_or_kw()?.value.clone();
        }
        // Skip optional braced block
        if self.check(TokenType::LBrace) {
            self.skip_braced_block()?;
        }
        Ok((
            Loc {
                line: tok.line,
                column: tok.column,
            },
            name,
            target,
            output_type,
        ))
    }

    fn parse_weave_step(&mut self) -> Result<FlowStep, ParseError> {
        let tok = self.current().clone();
        self.advance();
        let mut node = WeaveStep {
            sources: Vec::new(),
            target: String::new(),
            format_type: String::new(),
            priority: Vec::new(),
            style: String::new(),
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
        };
        if self.check(TokenType::LBrace) {
            self.advance();
            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
                let f = self.current().value.clone();
                self.advance();
                if self.check(TokenType::Colon) {
                    self.advance();
                    match f.as_str() {
                        "sources" => node.sources = self.parse_bracketed_identifiers()?,
                        "target" => node.target = self.consume_any_ident_or_kw()?.value.clone(),
                        "format" => {
                            node.format_type = self.consume_any_ident_or_kw()?.value.clone()
                        }
                        "priority" => node.priority = self.parse_bracketed_identifiers()?,
                        "style" => node.style = self.consume_any_ident_or_kw()?.value.clone(),
                        _ => self.skip_value(),
                    }
                }
            }
            if self.check(TokenType::RBrace) {
                self.advance();
            }
        }
        Ok(FlowStep::Weave(node))
    }

    fn parse_use_step(&mut self) -> Result<FlowStep, ParseError> {
        let tok = self.current().clone();
        self.advance();
        let tool_name = self.consume_any_ident_or_kw()?.value.clone();
        let mut argument = String::new();
        // "on" argument
        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
            let next = self.current().clone();
            if next.value == "on" {
                self.advance();
                argument = self.consume_any_ident_or_kw()?.value.clone();
            }
        }
        if self.check(TokenType::LBrace) {
            self.skip_braced_block()?;
        }
        Ok(FlowStep::UseTool(UseToolStep {
            tool_name,
            argument,
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
        }))
    }

    fn parse_remember_step(&mut self) -> Result<FlowStep, ParseError> {
        let tok = self.current().clone();
        self.advance();
        let expr = self.consume_any_ident_or_kw()?.value.clone();
        let mut mem = String::new();
        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
            let next = self.current().clone();
            if next.value == "in" || next.ttype == TokenType::In {
                self.advance();
                mem = self.consume_any_ident_or_kw()?.value.clone();
            }
        }
        Ok(FlowStep::Remember(RememberStep {
            expression: expr,
            memory_target: mem,
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
        }))
    }

    fn parse_recall_step(&mut self) -> Result<FlowStep, ParseError> {
        let tok = self.current().clone();
        self.advance();
        let query = if self.check(TokenType::StringLit) {
            self.consume(TokenType::StringLit)?.value.clone()
        } else {
            self.consume_any_ident_or_kw()?.value.clone()
        };
        let mut mem = String::new();
        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
            let next = self.current().clone();
            if next.value == "from" || next.ttype == TokenType::From {
                self.advance();
                mem = self.consume_any_ident_or_kw()?.value.clone();
            }
        }
        Ok(FlowStep::Recall(RecallStep {
            query,
            memory_source: mem,
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
        }))
    }

    fn parse_hibernate_step(&mut self) -> Result<FlowStep, ParseError> {
        let tok = self.current().clone();
        self.advance();
        let mut event = String::new();
        let mut timeout = String::new();
        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
            event = self.consume_any_ident_or_kw()?.value.clone();
        }
        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
            let next = self.current().clone();
            if next.ttype == TokenType::Duration {
                self.advance();
                timeout = next.value.clone();
            }
        }
        Ok(FlowStep::Hibernate(HibernateStep {
            event_name: event,
            timeout,
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
        }))
    }

    fn parse_associate_step(&mut self) -> Result<FlowStep, ParseError> {
        let tok = self.current().clone();
        self.advance();
        let left = self.consume_any_ident_or_kw()?.value.clone();
        let mut right = String::new();
        let mut using = String::new();
        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
            right = self.consume_any_ident_or_kw()?.value.clone();
        }
        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
            let next = self.current().clone();
            if next.value == "using" {
                self.advance();
                using = self.consume_any_ident_or_kw()?.value.clone();
            }
        }
        Ok(FlowStep::Associate(AssociateStep {
            left,
            right,
            using_field: using,
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
        }))
    }

    fn parse_aggregate_step(&mut self) -> Result<FlowStep, ParseError> {
        let tok = self.current().clone();
        self.advance();
        let target = self.consume_any_ident_or_kw()?.value.clone();
        let mut group_by = Vec::new();
        let mut alias = String::new();
        if self.check(TokenType::LBrace) {
            self.advance();
            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
                let f = self.current().value.clone();
                self.advance();
                if self.check(TokenType::Colon) {
                    self.advance();
                    match f.as_str() {
                        "group_by" => group_by = self.parse_bracketed_identifiers()?,
                        "alias" | "as" => alias = self.consume_any_ident_or_kw()?.value.clone(),
                        _ => self.skip_value(),
                    }
                }
            }
            if self.check(TokenType::RBrace) {
                self.advance();
            }
        }
        Ok(FlowStep::Aggregate(AggregateStep {
            target,
            group_by,
            alias,
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
        }))
    }

    fn parse_explore_step(&mut self) -> Result<FlowStep, ParseError> {
        let tok = self.current().clone();
        self.advance();
        let target = self.consume_any_ident_or_kw()?.value.clone();
        let mut limit = None;
        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
            if self.current().ttype == TokenType::Integer {
                limit = self.current().value.parse::<i64>().ok();
                self.advance();
            }
        }
        Ok(FlowStep::ExploreStep(ExploreStepNode {
            target,
            limit,
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
        }))
    }

    fn parse_ingest_step(&mut self) -> Result<FlowStep, ParseError> {
        let tok = self.current().clone();
        self.advance();
        let source = self.consume_any_ident_or_kw()?.value.clone();
        let mut target = String::new();
        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
            let next = self.current().clone();
            if next.value == "into" || next.ttype == TokenType::Into {
                self.advance();
                target = self.consume_any_ident_or_kw()?.value.clone();
            }
        }
        if self.check(TokenType::LBrace) {
            self.skip_braced_block()?;
        }
        Ok(FlowStep::Ingest(IngestStep {
            source,
            target,
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
        }))
    }

    fn parse_navigate_step(&mut self) -> Result<FlowStep, ParseError> {
        let tok = self.current().clone();
        self.advance();
        let pix_name = self.consume_any_ident_or_kw()?.value.clone();
        let mut node = NavigateStep {
            pix_name,
            corpus_name: String::new(),
            query_expr: String::new(),
            trail_enabled: false,
            output_name: String::new(),
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
        };
        if self.check(TokenType::LBrace) {
            self.advance();
            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
                let f = self.current().value.clone();
                self.advance();
                if self.check(TokenType::Colon) {
                    self.advance();
                    match f.as_str() {
                        "corpus" => {
                            node.corpus_name = self.consume_any_ident_or_kw()?.value.clone()
                        }
                        "query" => {
                            node.query_expr = self.consume(TokenType::StringLit)?.value.clone()
                        }
                        "trail" => {
                            node.trail_enabled = self.consume_any_ident_or_kw()?.value == "true"
                        }
                        "output" | "as" => {
                            node.output_name = self.consume_any_ident_or_kw()?.value.clone()
                        }
                        _ => self.skip_value(),
                    }
                }
            }
            if self.check(TokenType::RBrace) {
                self.advance();
            }
        }
        Ok(FlowStep::Navigate(node))
    }

    fn parse_drill_step(&mut self) -> Result<FlowStep, ParseError> {
        let tok = self.current().clone();
        self.advance();
        let pix_name = self.consume_any_ident_or_kw()?.value.clone();
        let mut node = DrillStep {
            pix_name,
            subtree_path: String::new(),
            query_expr: String::new(),
            output_name: String::new(),
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
        };
        if self.check(TokenType::LBrace) {
            self.advance();
            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
                let f = self.current().value.clone();
                self.advance();
                if self.check(TokenType::Colon) {
                    self.advance();
                    match f.as_str() {
                        "subtree" | "path" => {
                            node.subtree_path = self.consume(TokenType::StringLit)?.value.clone()
                        }
                        "query" => {
                            node.query_expr = self.consume(TokenType::StringLit)?.value.clone()
                        }
                        "output" | "as" => {
                            node.output_name = self.consume_any_ident_or_kw()?.value.clone()
                        }
                        _ => self.skip_value(),
                    }
                }
            }
            if self.check(TokenType::RBrace) {
                self.advance();
            }
        }
        Ok(FlowStep::Drill(node))
    }

    fn parse_corroborate_step(&mut self) -> Result<FlowStep, ParseError> {
        let tok = self.current().clone();
        self.advance();
        let nav_ref = self.consume_any_ident_or_kw()?.value.clone();
        let mut output = String::new();
        if self.check(TokenType::Arrow) {
            self.advance();
            output = self.consume_any_ident_or_kw()?.value.clone();
        }
        Ok(FlowStep::Corroborate(CorroborateStep {
            navigate_ref: nav_ref,
            output_name: output,
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
        }))
    }

    fn parse_listen_step(&mut self) -> Result<FlowStep, ParseError> {
        let tok = self.current().clone();
        self.advance();
        // §λ-L-E Fase 13 D4 — dual-mode listen:
        //   • String topic (legacy, deprecated since Fase 13)
        //   • Identifier (canonical: declared ChannelDefinition)
        let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
            (self.consume(TokenType::StringLit)?.value.clone(), false)
        } else {
            (self.consume_any_ident_or_kw()?.value.clone(), true)
        };
        let mut alias = String::new();
        if !self.at_declaration_start()
            && !self.check(TokenType::RBrace)
            && !self.check(TokenType::LBrace)
        {
            let next = self.current().clone();
            if next.value == "as" || next.ttype == TokenType::As {
                self.advance();
                alias = self.consume_any_ident_or_kw()?.value.clone();
            }
        }
        if self.check(TokenType::LBrace) {
            self.skip_braced_block()?;
        }
        Ok(FlowStep::Listen(ListenStep {
            channel,
            channel_is_ref,
            event_alias: alias,
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
        }))
    }

    fn parse_retrieve_step(&mut self) -> Result<FlowStep, ParseError> {
        let tok = self.current().clone();
        self.advance();
        let store = self.consume_any_ident_or_kw()?.value.clone();
        let mut where_expr = String::new();
        let mut alias = String::new();
        if self.check(TokenType::LBrace) {
            self.advance();
            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
                let f = self.current().value.clone();
                self.advance();
                if self.check(TokenType::Colon) {
                    self.advance();
                    match f.as_str() {
                        "where" => where_expr = self.consume(TokenType::StringLit)?.value.clone(),
                        "as" | "alias" => alias = self.consume_any_ident_or_kw()?.value.clone(),
                        _ => self.skip_value(),
                    }
                }
            }
            if self.check(TokenType::RBrace) {
                self.advance();
            }
        }
        Ok(FlowStep::Retrieve(RetrieveStep {
            store_name: store,
            where_expr,
            alias,
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
        }))
    }

    /// §Fase 35.m — Parse a `purge` step, capturing the optional
    /// `{ where: "<expr>" }` filter. (Fase 35.p moved `mutate` to its
    /// own `parse_mutate_step`, which also captures SET columns; this
    /// helper now serves `purge` alone — a `DELETE` has no SET clause.)
    ///
    /// Before Fase 35.m these two steps parsed via `parse_flow_step_simple`,
    /// which *skipped* the braced block — so a written `where:` clause
    /// was silently dropped and every `mutate`/`purge` ran against the
    /// whole store, leaving the entire Fase 35.b/c parameterized-filter
    /// machinery unreachable for them. This mirror of `parse_retrieve_step`
    /// (minus the `as:` alias — a mutate/purge binds no result) closes
    /// that gap. Returns `(loc, store_name, where_expr)`.
    fn parse_store_where_step(
        &mut self,
    ) -> Result<(Loc, String, String), ParseError> {
        let tok = self.current().clone();
        self.advance(); // consume the keyword
        let store = if self.at_declaration_start()
            || self.check(TokenType::RBrace)
            || self.check(TokenType::Eof)
        {
            String::new()
        } else {
            self.consume_any_ident_or_kw()?.value.clone()
        };
        let mut where_expr = String::new();
        if self.check(TokenType::LBrace) {
            self.advance();
            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
                let field = self.current().value.clone();
                self.advance();
                if self.check(TokenType::Colon) {
                    self.advance();
                    match field.as_str() {
                        "where" => {
                            where_expr =
                                self.consume(TokenType::StringLit)?.value.clone()
                        }
                        _ => self.skip_value(),
                    }
                }
            }
            if self.check(TokenType::RBrace) {
                self.advance();
            }
        }
        Ok((
            Loc {
                line: tok.line,
                column: tok.column,
            },
            store,
            where_expr,
        ))
    }

    /// §Fase 35.o — Parse a `persist` step, capturing the optional
    /// `{ col: value }` field block.
    ///
    /// Before Fase 35.o `persist` parsed via `parse_flow_step_simple`,
    /// which *skipped* the braced block — so a written field block was
    /// silently dropped and the runtime fell back to writing every
    /// context binding as a row, which fails against any real table
    /// (flows always carry more bindings than a table has columns).
    /// This captures the declared columns into `PersistStep.fields`;
    /// the runtime writes exactly those (interpolated). A `persist`
    /// with no block keeps the v1.30.0 user-bindings fallback — fully
    /// backward-compatible. Mirror of `parse_retrieve_step`, but the
    /// keys are arbitrary column names rather than the fixed
    /// `where:` / `as:` filter keys.
    ///
    /// The optional `into` connector (`persist into <store>`) is
    /// accepted and skipped — before Fase 35.o `into` was captured as
    /// the store name.
    fn parse_persist_step(&mut self) -> Result<FlowStep, ParseError> {
        let tok = self.current().clone();
        self.advance(); // consume `persist`
        // Optional `into` connector — skip it so the store name that
        // follows is not mistaken for the target.
        if self.current().value == "into" && !self.check(TokenType::LBrace) {
            self.advance();
        }
        let store = if self.at_declaration_start()
            || self.check(TokenType::LBrace)
            || self.check(TokenType::RBrace)
            || self.check(TokenType::Eof)
        {
            String::new()
        } else {
            self.consume_any_ident_or_kw()?.value.clone()
        };
        let mut fields: Vec<(String, String)> = Vec::new();
        if self.check(TokenType::LBrace) {
            self.advance();
            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
                let col = self.current().value.clone();
                self.advance();
                if self.check(TokenType::Colon) {
                    self.advance();
                    let value = if self.check(TokenType::StringLit) {
                        self.consume(TokenType::StringLit)?.value.clone()
                    } else if self.check(TokenType::RBrace)
                        || self.check(TokenType::Eof)
                        || self.check(TokenType::Colon)
                    {
                        String::new()
                    } else {
                        let v = self.current().clone();
                        self.advance();
                        v.value.clone()
                    };
                    fields.push((col, value));
                }
            }
            if self.check(TokenType::RBrace) {
                self.advance();
            }
        }
        Ok(FlowStep::Persist(PersistStep {
            store_name: store,
            fields,
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
        }))
    }

    /// §Fase 35.p — Parse a `mutate` step, capturing both the
    /// `{ where: "<expr>" }` filter AND the `{ col: value }` SET
    /// assignments.
    ///
    /// Before Fase 35.p `mutate` parsed via `parse_store_where_step`,
    /// which captured only `where:` and *skipped* every other key — so
    /// the runtime built the `UPDATE … SET` clause from every flow
    /// binding (params + step results + `let`s), which fails against
    /// any real table (`column "X" does not exist`). This closes the
    /// gap symmetrically to 35.o's `persist` block: every key other
    /// than `where:` is a SET column; a `mutate` with no SET column
    /// keeps the v1.31.0 user-bindings fallback. `where:` keeps its
    /// string-literal grammar (as in `retrieve` / `purge`).
    fn parse_mutate_step(&mut self) -> Result<FlowStep, ParseError> {
        let tok = self.current().clone();
        self.advance(); // consume `mutate`
        let store = if self.at_declaration_start()
            || self.check(TokenType::LBrace)
            || self.check(TokenType::RBrace)
            || self.check(TokenType::Eof)
        {
            String::new()
        } else {
            self.consume_any_ident_or_kw()?.value.clone()
        };
        let mut where_expr = String::new();
        let mut fields: Vec<(String, String)> = Vec::new();
        if self.check(TokenType::LBrace) {
            self.advance();
            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
                let key = self.current().value.clone();
                self.advance();
                if self.check(TokenType::Colon) {
                    self.advance();
                    if key == "where" {
                        where_expr =
                            self.consume(TokenType::StringLit)?.value.clone();
                    } else {
                        let value = if self.check(TokenType::StringLit) {
                            self.consume(TokenType::StringLit)?.value.clone()
                        } else if self.check(TokenType::RBrace)
                            || self.check(TokenType::Eof)
                            || self.check(TokenType::Colon)
                        {
                            String::new()
                        } else {
                            let v = self.current().clone();
                            self.advance();
                            v.value.clone()
                        };
                        fields.push((key, value));
                    }
                }
            }
            if self.check(TokenType::RBrace) {
                self.advance();
            }
        }
        Ok(FlowStep::Mutate(MutateStep {
            store_name: store,
            where_expr,
            fields,
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
        }))
    }

    // ── TIER 2 DECLARATIONS ────────────────────────────────────────

    fn parse_agent(&mut self) -> Result<AgentDefinition, ParseError> {
        let tok = self.consume(TokenType::Agent)?;
        let name = self.consume(TokenType::Identifier)?.value;
        let mut node = AgentDefinition {
            name,
            goal: String::new(),
            tools: Vec::new(),
            memory_ref: String::new(),
            strategy: String::new(),
            on_stuck: String::new(),
            shield_ref: String::new(),
            max_iterations: None,
            max_tokens: None,
            max_time: String::new(),
            max_cost: None,
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };
        // Skip optional parameters/return type before brace
        while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
            self.advance();
        }
        self.consume(TokenType::LBrace)?;
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let field = self.current().clone();
            let field_name = field.value.clone();
            self.advance();
            if self.check(TokenType::Colon) {
                self.advance();
                match field_name.as_str() {
                    "goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
                    "tools" => node.tools = self.parse_bracketed_identifiers()?,
                    "memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
                    "on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
                    "max_iterations" => node.max_iterations = self.parse_optional_int(),
                    "max_tokens" => node.max_tokens = self.parse_optional_int(),
                    "max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
                    "max_cost" => node.max_cost = self.parse_optional_float(),
                    _ => self.skip_value(),
                }
            } else if self.check(TokenType::LBrace) {
                self.skip_braced_block()?;
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    /// §Fase 53 — `extension Name { category: effects|scan, members: [ … ] }`.
    /// The parser is permissive on field/category VALUES (validated in
    /// §53.c by the type-checker — no-shadowing, category-membership);
    /// it only enforces the structural grammar here.
    fn parse_extension(&mut self) -> Result<ExtensionDefinition, ParseError> {
        let tok = self.consume(TokenType::Extension)?;
        let name = self.consume(TokenType::Identifier)?.value;
        let mut node = ExtensionDefinition {
            name,
            category: String::new(),
            members: Vec::new(),
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };
        self.consume(TokenType::LBrace)?;
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let field_name = self.current().value.clone();
            self.advance();
            if self.check(TokenType::Colon) {
                self.advance();
                match field_name.as_str() {
                    "category" => {
                        node.category = self.consume_any_ident_or_kw()?.value.clone()
                    }
                    "members" => node.members = self.parse_extension_members()?,
                    _ => self.skip_value(),
                }
            } else if self.check(TokenType::LBrace) {
                self.skip_braced_block()?;
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    /// §Fase 53 — parse `[ "name" [ : { semantics: "…", default_confidence: 0.8 } ], … ]`.
    /// Each member is a string literal optionally followed by a metadata
    /// block. Trailing/interleaved commas are tolerated.
    fn parse_extension_members(&mut self) -> Result<Vec<ExtensionMember>, ParseError> {
        let mut members = Vec::new();
        self.consume(TokenType::LBracket)?;
        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
            let name_tok = self.consume(TokenType::StringLit)?;
            let mut member = ExtensionMember {
                name: name_tok.value.clone(),
                semantics: None,
                default_confidence: None,
                loc: Loc {
                    line: name_tok.line,
                    column: name_tok.column,
                },
            };
            // Optional `: { semantics: "…", default_confidence: 0.8 }`.
            if self.check(TokenType::Colon) {
                self.advance();
                self.consume(TokenType::LBrace)?;
                while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
                    let mkey = self.current().value.clone();
                    self.advance();
                    if self.check(TokenType::Colon) {
                        self.advance();
                        match mkey.as_str() {
                            "semantics" => {
                                member.semantics =
                                    Some(self.consume(TokenType::StringLit)?.value.clone())
                            }
                            "default_confidence" => {
                                member.default_confidence = self.parse_optional_float()
                            }
                            _ => self.skip_value(),
                        }
                    }
                    if self.check(TokenType::Comma) {
                        self.advance();
                    }
                }
                self.consume(TokenType::RBrace)?;
            }
            members.push(member);
            if self.check(TokenType::Comma) {
                self.advance();
            }
        }
        self.consume(TokenType::RBracket)?;
        Ok(members)
    }

    fn parse_shield(&mut self) -> Result<ShieldDefinition, ParseError> {
        let tok = self.consume(TokenType::Shield)?;
        let name = self.consume(TokenType::Identifier)?.value;
        let mut node = ShieldDefinition {
            name,
            scan: Vec::new(),
            strategy: String::new(),
            on_breach: String::new(),
            severity: String::new(),
            quarantine: String::new(),
            max_retries: None,
            confidence_threshold: None,
            allow_tools: Vec::new(),
            deny_tools: Vec::new(),
            sandbox: None,
            redact: Vec::new(),
            log: String::new(),
            deflect_message: String::new(),
            taint: String::new(),
            compliance: Vec::new(),
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };
        self.consume(TokenType::LBrace)?;
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let field_name = self.current().value.clone();
            self.advance();
            if self.check(TokenType::Colon) {
                self.advance();
                match field_name.as_str() {
                    "scan" => node.scan = self.parse_bracketed_identifiers()?,
                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
                    "on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
                    "severity" => node.severity = self.consume_any_ident_or_kw()?.value.clone(),
                    "quarantine" => {
                        node.quarantine = self.consume(TokenType::StringLit)?.value.clone()
                    }
                    "max_retries" => node.max_retries = self.parse_optional_int(),
                    "confidence_threshold" => {
                        node.confidence_threshold = self.parse_optional_float()
                    }
                    "allow_tools" => node.allow_tools = self.parse_bracketed_identifiers()?,
                    "deny_tools" => node.deny_tools = self.parse_bracketed_identifiers()?,
                    "sandbox" => {
                        node.sandbox = Some(self.consume_any_ident_or_kw()?.value == "true")
                    }
                    "redact" => node.redact = self.parse_bracketed_identifiers()?,
                    "log" => node.log = self.consume_any_ident_or_kw()?.value.clone(),
                    "deflect_message" => {
                        node.deflect_message = self.consume(TokenType::StringLit)?.value.clone()
                    }
                    "taint" => node.taint = self.consume_any_ident_or_kw()?.value.clone(),
                    // ESK Fase 6.1 — covered regulatory classes.
                    "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
                    _ => self.skip_value(),
                }
            } else if self.check(TokenType::LBrace) {
                self.skip_braced_block()?;
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    fn parse_pix(&mut self) -> Result<PixDefinition, ParseError> {
        let tok = self.consume(TokenType::Pix)?;
        let name = self.consume(TokenType::Identifier)?.value;
        let mut node = PixDefinition {
            name,
            source: String::new(),
            depth: None,
            branching: None,
            model: String::new(),
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };
        self.consume(TokenType::LBrace)?;
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let field_name = self.current().value.clone();
            self.advance();
            if self.check(TokenType::Colon) {
                self.advance();
                match field_name.as_str() {
                    "source" => node.source = self.consume(TokenType::StringLit)?.value.clone(),
                    "depth" => node.depth = self.parse_optional_int(),
                    "branching" => node.branching = self.parse_optional_int(),
                    "model" => node.model = self.consume_any_ident_or_kw()?.value.clone(),
                    _ => self.skip_value(),
                }
            } else if self.check(TokenType::LBrace) {
                self.skip_braced_block()?;
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    fn parse_psyche(&mut self) -> Result<PsycheDefinition, ParseError> {
        let tok = self.consume(TokenType::Psyche)?;
        let name = self.consume(TokenType::Identifier)?.value;
        let mut node = PsycheDefinition {
            name,
            dimensions: Vec::new(),
            manifold_noise: None,
            manifold_momentum: None,
            safety_constraints: Vec::new(),
            quantum_enabled: None,
            inference_mode: String::new(),
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };
        self.consume(TokenType::LBrace)?;
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let field_name = self.current().value.clone();
            self.advance();
            if self.check(TokenType::Colon) {
                self.advance();
                match field_name.as_str() {
                    "dimensions" => node.dimensions = self.parse_bracketed_identifiers()?,
                    "manifold_noise" => node.manifold_noise = self.parse_optional_float(),
                    "manifold_momentum" => node.manifold_momentum = self.parse_optional_float(),
                    "safety_constraints" => {
                        node.safety_constraints = self.parse_bracketed_identifiers()?
                    }
                    "quantum_enabled" => {
                        node.quantum_enabled = Some(self.consume_any_ident_or_kw()?.value == "true")
                    }
                    "inference_mode" => {
                        node.inference_mode = self.consume_any_ident_or_kw()?.value.clone()
                    }
                    _ => self.skip_value(),
                }
            } else if self.check(TokenType::LBrace) {
                self.skip_braced_block()?;
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    fn parse_corpus(&mut self) -> Result<CorpusDefinition, ParseError> {
        let tok = self.consume(TokenType::Corpus)?;
        let name = self.consume(TokenType::Identifier)?.value;
        let mut node = CorpusDefinition {
            name,
            documents: Vec::new(),
            mcp_server: String::new(),
            mcp_resource_uri: String::new(),
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };
        // corpus Name from mcp("server", "uri") — short form
        if self.check(TokenType::From) {
            self.advance();
            self.consume(TokenType::Mcp)?;
            self.consume(TokenType::LParen)?;
            node.mcp_server = self.consume(TokenType::StringLit)?.value.clone();
            self.consume(TokenType::Comma)?;
            node.mcp_resource_uri = self.consume(TokenType::StringLit)?.value.clone();
            self.consume(TokenType::RParen)?;
            return Ok(node);
        }
        self.consume(TokenType::LBrace)?;
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let field_name = self.current().value.clone();
            self.advance();
            if self.check(TokenType::Colon) {
                self.advance();
                match field_name.as_str() {
                    "documents" => node.documents = self.parse_bracketed_identifiers()?,
                    _ => self.skip_value(),
                }
            } else if self.check(TokenType::LBrace) {
                self.skip_braced_block()?;
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    fn parse_dataspace(&mut self) -> Result<DataspaceDefinition, ParseError> {
        let tok = self.consume(TokenType::Dataspace)?;
        let name = self.consume(TokenType::Identifier)?.value;
        let node = DataspaceDefinition {
            name,
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };
        if self.check(TokenType::LBrace) {
            self.skip_braced_block()?;
        }
        Ok(node)
    }

    fn parse_ots(&mut self) -> Result<OtsDefinition, ParseError> {
        let tok = self.consume(TokenType::Ots)?;
        let name = self.consume(TokenType::Identifier)?.value;
        let mut node = OtsDefinition {
            name,
            teleology: String::new(),
            homotopy_search: String::new(),
            loss_function: String::new(),
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };
        // Skip optional type params <In, Out>
        if self.check(TokenType::Lt) {
            while !self.check(TokenType::Gt) && !self.check(TokenType::Eof) {
                self.advance();
            }
            if self.check(TokenType::Gt) {
                self.advance();
            }
        }
        self.consume(TokenType::LBrace)?;
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let field_name = self.current().value.clone();
            self.advance();
            if self.check(TokenType::Colon) {
                self.advance();
                match field_name.as_str() {
                    "teleology" => {
                        node.teleology = self.consume(TokenType::StringLit)?.value.clone()
                    }
                    "homotopy_search" => {
                        node.homotopy_search = self.consume_any_ident_or_kw()?.value.clone()
                    }
                    "loss_function" => {
                        node.loss_function = self.consume(TokenType::StringLit)?.value.clone()
                    }
                    _ => self.skip_value(),
                }
            } else if self.check(TokenType::LBrace) {
                self.skip_braced_block()?;
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    fn parse_mandate(&mut self) -> Result<MandateDefinition, ParseError> {
        let tok = self.consume(TokenType::Mandate)?;
        let name = self.consume(TokenType::Identifier)?.value;
        let mut node = MandateDefinition {
            name,
            constraint: String::new(),
            kp: None,
            ki: None,
            kd: None,
            tolerance: None,
            max_steps: None,
            on_violation: String::new(),
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };
        self.consume(TokenType::LBrace)?;
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let field_name = self.current().value.clone();
            self.advance();
            if self.check(TokenType::Colon) {
                self.advance();
                match field_name.as_str() {
                    "constraint" => {
                        node.constraint = self.consume(TokenType::StringLit)?.value.clone()
                    }
                    "kp" | "Kp" => node.kp = self.parse_optional_float(),
                    "ki" | "Ki" => node.ki = self.parse_optional_float(),
                    "kd" | "Kd" => node.kd = self.parse_optional_float(),
                    "tolerance" => node.tolerance = self.parse_optional_float(),
                    "max_steps" => node.max_steps = self.parse_optional_int(),
                    "on_violation" => {
                        node.on_violation = self.consume_any_ident_or_kw()?.value.clone()
                    }
                    _ => self.skip_value(),
                }
            } else if self.check(TokenType::LBrace) {
                self.skip_braced_block()?;
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    fn parse_compute(&mut self) -> Result<ComputeDefinition, ParseError> {
        let tok = self.consume(TokenType::Compute)?;
        let name = self.consume(TokenType::Identifier)?.value;
        let mut node = ComputeDefinition {
            name,
            shield_ref: String::new(),
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };
        // Skip optional parameters/return type before brace
        while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
            self.advance();
        }
        self.consume(TokenType::LBrace)?;
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let field_name = self.current().value.clone();
            self.advance();
            if self.check(TokenType::Colon) {
                self.advance();
                match field_name.as_str() {
                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
                    _ => self.skip_value(),
                }
            } else if self.check(TokenType::LBrace) {
                self.skip_braced_block()?;
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    fn parse_daemon(&mut self) -> Result<DaemonDefinition, ParseError> {
        let tok = self.consume(TokenType::Daemon)?;
        let name = self.consume(TokenType::Identifier)?.value;
        let mut node = DaemonDefinition {
            name,
            goal: String::new(),
            tools: Vec::new(),
            memory_ref: String::new(),
            strategy: String::new(),
            on_stuck: String::new(),
            shield_ref: String::new(),
            max_tokens: None,
            max_time: String::new(),
            max_cost: None,
            listeners: Vec::new(),
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };
        // Skip optional parameters/return type before brace
        while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
            self.advance();
        }
        self.consume(TokenType::LBrace)?;
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let field = self.current().clone();
            let field_name = field.value.clone();
            self.advance();
            if self.check(TokenType::Colon) {
                self.advance();
                match field_name.as_str() {
                    "goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
                    "tools" => node.tools = self.parse_bracketed_identifiers()?,
                    "memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
                    "on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
                    "max_tokens" => node.max_tokens = self.parse_optional_int(),
                    "max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
                    "max_cost" => node.max_cost = self.parse_optional_float(),
                    _ => self.skip_value(),
                }
            } else if field.ttype == TokenType::Listen {
                // §λ-L-E Fase 13 D4 — preserve listen blocks for type
                // checking.  We backtracked past the `listen` keyword
                // by `advance()` above, so reconstruct a synthetic
                // listener using the same dual-mode dispatch the flow
                // step parser uses (string topic OR typed channel ref).
                let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
                    (self.consume(TokenType::StringLit)?.value.clone(), false)
                } else {
                    (self.consume_any_ident_or_kw()?.value.clone(), true)
                };
                let mut alias = String::new();
                if !self.at_declaration_start()
                    && !self.check(TokenType::RBrace)
                    && !self.check(TokenType::LBrace)
                {
                    let next = self.current().clone();
                    if next.value == "as" || next.ttype == TokenType::As {
                        self.advance();
                        alias = self.consume_any_ident_or_kw()?.value.clone();
                    }
                }
                let listen_loc = Loc {
                    line: field.line,
                    column: field.column,
                };
                if self.check(TokenType::LBrace) {
                    self.skip_braced_block()?;
                }
                node.listeners.push(ListenStep {
                    channel,
                    channel_is_ref,
                    event_alias: alias,
                    loc: listen_loc,
                });
            } else if self.check(TokenType::LBrace) {
                self.skip_braced_block()?;
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    fn parse_axonstore(&mut self) -> Result<AxonStoreDefinition, ParseError> {
        let tok = self.consume(TokenType::AxonStore)?;
        let name = self.consume(TokenType::Identifier)?.value;
        let mut node = AxonStoreDefinition {
            name,
            backend: String::new(),
            connection: String::new(),
            confidence_floor: None,
            isolation: String::new(),
            on_breach: String::new(),
            capability: String::new(),
            column_schema: None,
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };
        self.consume(TokenType::LBrace)?;
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let field = self.current().clone();
            let field_name = field.value.clone();
            // §Fase 38.b (D1) — `schema:` declaration in three closed
            // forms: inline column block, manifest reference (string
            // literal), or env-var schema namespace (`env:VAR` —
            // unquoted or quoted). Parse the form; the §38.d / §38.e
            // type-checker consumes the resulting AST.
            if field.ttype == TokenType::Schema {
                self.advance();
                let parsed = self.parse_store_schema_declaration(&node.name, field.line, field.column)?;
                node.column_schema = Some(parsed);
                continue;
            }
            self.advance();
            if self.check(TokenType::Colon) {
                self.advance();
                match field_name.as_str() {
                    "backend" => node.backend = self.consume_any_ident_or_kw()?.value.clone(),
                    "connection" => {
                        node.connection = self.consume(TokenType::StringLit)?.value.clone()
                    }
                    "confidence_floor" => node.confidence_floor = self.parse_optional_float(),
                    "isolation" => node.isolation = self.consume_any_ident_or_kw()?.value.clone(),
                    "on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
                    // §Fase 35.j (D11) — Pillar IV: the capability slug
                    // required to access this store. Validated against
                    // the closed slug grammar shared with `requires:`.
                    "capability" => {
                        let slug_tok = self.consume(TokenType::StringLit)?.clone();
                        if !is_valid_capability_slug(&slug_tok.value) {
                            return Err(ParseError {
                                message: format!(
                                    "Invalid capability slug '{}' in axonstore '{}' \
                                     `capability:`. Capability slugs must match \
                                     ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
                                     lowercase identifiers starting with a letter. Examples: \
                                     `admin`, `tenant.read`, `hipaa.phi.read`.",
                                    slug_tok.value, node.name
                                ),
                                line: slug_tok.line,
                                column: slug_tok.column,
                                ..Default::default()
                            });
                        }
                        node.capability = slug_tok.value.clone();
                    }
                    _ => self.skip_value(),
                }
            } else if self.check(TokenType::LBrace) {
                self.skip_braced_block()?;
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    /// §Fase 38.b (D1) — parse the three closed forms of an `axonstore`
    /// `schema:` declaration:
    ///
    ///   * form (a) **inline** — `schema { col: Type [constraint…], … }`
    ///   * form (b) **manifest reference** — `schema: "qualified.name"`
    ///     (string literal that does NOT start with `env:`)
    ///   * form (c) **env-var schema namespace** — `schema: env:VAR`
    ///     (unquoted) OR `schema: "env:VAR"` (quoted; the literal
    ///     starts with `env:`)
    ///
    /// Called immediately AFTER `schema` is consumed.
    fn parse_store_schema_declaration(
        &mut self,
        store_name: &str,
        sch_line: u32,
        sch_col: u32,
    ) -> Result<crate::store_schema::StoreColumnSchema, ParseError> {
        use crate::store_schema::{StoreColumn, StoreColumnSchema, StoreColumnType};

        // — Form (a) — inline column block: `schema { ... }`. —
        if self.check(TokenType::LBrace) {
            self.consume(TokenType::LBrace)?;
            let mut columns: Vec<StoreColumn> = Vec::new();
            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
                let col_tok = self.current().clone();
                let col_name = self.consume_any_ident_or_kw()?.value.clone();
                self.consume(TokenType::Colon)?;
                let type_tok = self.consume_any_ident_or_kw()?.clone();
                let col_type = StoreColumnType::from_token(&type_tok.value).ok_or_else(|| {
                    let names = StoreColumnType::all_canonical_names();
                    let suggestion =
                        crate::smart_suggest::suggest_for(&type_tok.value, &names);
                    let suggest_suffix = if suggestion.is_empty() {
                        String::new()
                    } else {
                        format!(" {suggestion}")
                    };
                    let known = names.join(", ");
                    ParseError {
                        message: format!(
                            "Unknown column type `{}` for column `{}` in \
                             axonstore `{}` `schema:` block. The closed \
                             v1.38.0 column-type catalog (Fase 38.b D1) \
                             is {{{known}}} (plus common lowercase \
                             aliases — `int`/`integer`/`int4` for \
                             `Int`, `bool`/`boolean` for `Bool`, etc.).\
                             {suggest_suffix}",
                            type_tok.value, col_name, store_name
                        ),
                        line: type_tok.line,
                        column: type_tok.column,
                        ..Default::default()
                    }
                })?;

                let mut col = StoreColumn {
                    name: col_name,
                    col_type,
                    primary_key: false,
                    auto_increment: false,
                    not_null: false,
                    unique: false,
                    default_value: String::new(),
                    // §Fase 38.x.d (D1) — `identity` is now a recognized
                    // inline keyword (see the constraint loop below).
                    // Defaults to false; set to true when the adopter
                    // writes `id: BigInt primary_key identity`.
                    identity: false,
                    line: col_tok.line,
                    column: col_tok.column,
                };

                // Trailing constraints (position-independent), matching
                // the Python `_parse_store_column` surface.
                while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
                    if self.current().ttype != TokenType::Identifier {
                        // The next column starts with a non-identifier
                        // (rare) — stop the constraint scan.
                        break;
                    }
                    let constraint = self.current().value.clone();
                    match constraint.as_str() {
                        "primary_key" => {
                            col.primary_key = true;
                            self.advance();
                        }
                        "auto_increment" => {
                            col.auto_increment = true;
                            self.advance();
                        }
                        "not_null" => {
                            col.not_null = true;
                            self.advance();
                        }
                        "unique" => {
                            col.unique = true;
                            self.advance();
                        }
                        // §Fase 38.x.d (D1) — `identity` marks a column
                        // as `GENERATED ALWAYS/BY DEFAULT AS IDENTITY`.
                        // Distinct from `auto_increment` (legacy SERIAL
                        // via `nextval(...)` default). T803 skips
                        // identity columns from the NOT-NULL-omission
                        // check because Postgres auto-fills them; the
                        // distinction matters because IDENTITY ALWAYS
                        // also rejects user-supplied values, where
                        // SERIAL accepts them (a future 38.x.e arm in
                        // T802 may surface this).
                        "identity" => {
                            col.identity = true;
                            self.advance();
                        }
                        "default" => {
                            self.advance();
                            let dv = self.current().clone();
                            if matches!(
                                dv.ttype,
                                TokenType::StringLit
                                    | TokenType::Integer
                                    | TokenType::Float
                            ) {
                                col.default_value = dv.value.clone();
                                self.advance();
                            } else {
                                col.default_value =
                                    self.consume_any_ident_or_kw()?.value.clone();
                            }
                        }
                        _ => break,
                    }
                }

                columns.push(col);
            }
            self.consume(TokenType::RBrace)?;
            return Ok(StoreColumnSchema::Inline {
                columns,
                leading_trivia: Vec::new(),
                line: sch_line,
                column: sch_col,
            });
        }

        // — Forms (b) + (c) require a `:` separator. —
        if !self.check(TokenType::Colon) {
            let cur = self.current().clone();
            return Err(ParseError {
                message: format!(
                    "axonstore `{store_name}` `schema:` declaration expects \
                     `{{ … }}` (inline columns), `: \"manifest.ref\"` \
                     (manifest reference), or `: env:VAR` (per-tenant schema \
                     namespace). Got `{}` instead.",
                    cur.value
                ),
                line: cur.line,
                column: cur.column,
                ..Default::default()
            });
        }
        self.consume(TokenType::Colon)?;

        // — Form (b) or (c)-quoted — string literal value. —
        if self.check(TokenType::StringLit) {
            let lit = self.consume(TokenType::StringLit)?.clone();
            let value = lit.value.clone();
            if let Some(var) = value.strip_prefix("env:") {
                let var = var.trim();
                if var.is_empty() {
                    return Err(ParseError {
                        message: format!(
                            "axonstore `{store_name}` `schema: \"env:\"` is \
                             missing the variable name after the `env:` \
                             prefix."
                        ),
                        line: lit.line,
                        column: lit.column,
                        ..Default::default()
                    });
                }
                return Ok(StoreColumnSchema::EnvVar {
                    var_name: var.to_string(),
                    line: sch_line,
                    column: sch_col,
                });
            }
            // Plain string → manifest reference.
            if value.trim().is_empty() {
                return Err(ParseError {
                    message: format!(
                        "axonstore `{store_name}` `schema:` manifest reference \
                         is empty. Expected `\"qualified.name\"` — e.g. \
                         `\"public.tenants\"`."
                    ),
                    line: lit.line,
                    column: lit.column,
                    ..Default::default()
                });
            }
            return Ok(StoreColumnSchema::ManifestRef {
                qualified_name: value,
                line: sch_line,
                column: sch_col,
            });
        }

        // — Form (c) unquoted — `env:VAR`. The lexer emits `env` as an
        //   identifier, then `:`, then the identifier var name. —
        let env_tok = self.current().clone();
        if env_tok.value == "env" {
            self.advance();
            if !self.check(TokenType::Colon) {
                return Err(ParseError {
                    message: format!(
                        "axonstore `{store_name}` `schema: env` is missing the \
                         `:` separator. Expected `schema: env:VAR`."
                    ),
                    line: env_tok.line,
                    column: env_tok.column,
                    ..Default::default()
                });
            }
            self.advance(); // past ':'
            let var_tok = self.consume_any_ident_or_kw()?.clone();
            if var_tok.value.trim().is_empty() {
                return Err(ParseError {
                    message: format!(
                        "axonstore `{store_name}` `schema: env:` is missing \
                         the variable name."
                    ),
                    line: var_tok.line,
                    column: var_tok.column,
                    ..Default::default()
                });
            }
            return Ok(StoreColumnSchema::EnvVar {
                var_name: var_tok.value.clone(),
                line: sch_line,
                column: sch_col,
            });
        }

        Err(ParseError {
            message: format!(
                "axonstore `{store_name}` `schema:` declaration expects \
                 `{{ … }}` (inline columns), `\"manifest.ref\"` (manifest \
                 reference), or `env:VAR` (per-tenant schema namespace). \
                 Got `{}` instead.",
                env_tok.value
            ),
            line: env_tok.line,
            column: env_tok.column,
            ..Default::default()
        })
    }

    // ── §λ-L-E Fase 1 — Resource primitive ────────────────────────

    /// Parse: `resource Name { kind, endpoint, capacity, lifetime, certainty_floor, shield }`.
    ///
    /// Mirrors `axon.compiler.parser.Parser._parse_resource`. Unknown fields
    /// are silently skipped (keeps the grammar forward-compatible).
    fn parse_resource(&mut self) -> Result<ResourceDefinition, ParseError> {
        let tok = self.consume(TokenType::Resource)?;
        let name = self.consume(TokenType::Identifier)?.value;
        let mut node = ResourceDefinition {
            name,
            kind: String::new(),
            endpoint: String::new(),
            capacity: None,
            lifetime: "affine".to_string(),
            certainty_floor: None,
            shield_ref: String::new(),
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };
        self.consume(TokenType::LBrace)?;
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let field_tok = self.current().clone();
            let field_name = field_tok.value.clone();
            self.advance();
            if !self.check(TokenType::Colon) {
                // Tolerate stray brace or unknown layout.
                if self.check(TokenType::LBrace) {
                    self.skip_braced_block()?;
                }
                continue;
            }
            self.advance(); // past ':'
            match field_name.as_str() {
                "kind" => node.kind = self.consume_any_ident_or_kw()?.value,
                "endpoint" => node.endpoint = self.consume(TokenType::StringLit)?.value,
                "capacity" => {
                    node.capacity = self.parse_optional_int();
                }
                "lifetime" => {
                    let lt_tok = self.consume_any_ident_or_kw()?;
                    let lt = lt_tok.value;
                    if !matches!(lt.as_str(), "linear" | "affine" | "persistent") {
                        return Err(ParseError {
                            message: format!(
                                "Invalid lifetime '{lt}' in resource '{}' — \
                                 expected linear | affine | persistent",
                                node.name
                            ),
                            line: lt_tok.line,
                            column: lt_tok.column,
                                                    ..Default::default()
                        });
                    }
                    node.lifetime = lt;
                }
                "certainty_floor" => {
                    node.certainty_floor = self.parse_optional_float();
                }
                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
                _ => self.skip_value(),
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    /// Parse: `fabric Name { provider, region, zones, ephemeral, shield }`.
    fn parse_fabric(&mut self) -> Result<FabricDefinition, ParseError> {
        let tok = self.consume(TokenType::Fabric)?;
        let name = self.consume(TokenType::Identifier)?.value;
        let mut node = FabricDefinition {
            name,
            provider: String::new(),
            region: String::new(),
            zones: None,
            ephemeral: None,
            shield_ref: String::new(),
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };
        self.consume(TokenType::LBrace)?;
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let field_name = self.current().value.clone();
            self.advance();
            if !self.check(TokenType::Colon) {
                if self.check(TokenType::LBrace) {
                    self.skip_braced_block()?;
                }
                continue;
            }
            self.advance(); // past ':'
            match field_name.as_str() {
                "provider" => node.provider = self.consume_any_ident_or_kw()?.value,
                "region" => node.region = self.consume(TokenType::StringLit)?.value,
                "zones" => node.zones = self.parse_optional_int(),
                "ephemeral" => {
                    let b = self.parse_bool()?;
                    node.ephemeral = Some(b);
                }
                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
                _ => self.skip_value(),
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    /// Parse: `manifest Name { resources, fabric, region, zones, compliance }`.
    fn parse_manifest(&mut self) -> Result<ManifestDefinition, ParseError> {
        let tok = self.consume(TokenType::Manifest)?;
        let name = self.consume(TokenType::Identifier)?.value;
        let mut node = ManifestDefinition {
            name,
            resources: Vec::new(),
            fabric_ref: String::new(),
            region: String::new(),
            zones: None,
            compliance: Vec::new(),
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };
        self.consume(TokenType::LBrace)?;
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let field_name = self.current().value.clone();
            self.advance();
            if !self.check(TokenType::Colon) {
                if self.check(TokenType::LBrace) {
                    self.skip_braced_block()?;
                }
                continue;
            }
            self.advance();
            match field_name.as_str() {
                "resources" => node.resources = self.parse_bracketed_identifiers()?,
                "fabric" => node.fabric_ref = self.consume_any_ident_or_kw()?.value,
                "region" => node.region = self.consume(TokenType::StringLit)?.value,
                "zones" => node.zones = self.parse_optional_int(),
                "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
                _ => self.skip_value(),
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    /// Parse: `observe Name from Manifest { sources, quorum, timeout, on_partition, certainty_floor }`.
    fn parse_observe(&mut self) -> Result<ObserveDefinition, ParseError> {
        let tok = self.consume(TokenType::Observe)?;
        let name = self.consume(TokenType::Identifier)?.value;
        // `from <Manifest>` — required per Python grammar.
        self.consume(TokenType::From)?;
        let target = self.consume(TokenType::Identifier)?.value;
        let mut node = ObserveDefinition {
            name,
            target,
            sources: Vec::new(),
            quorum: None,
            timeout: String::new(),
            on_partition: "fail".to_string(),
            certainty_floor: None,
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };
        self.consume(TokenType::LBrace)?;
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let field_name = self.current().value.clone();
            self.advance();
            if !self.check(TokenType::Colon) {
                if self.check(TokenType::LBrace) {
                    self.skip_braced_block()?;
                }
                continue;
            }
            self.advance();
            match field_name.as_str() {
                "sources" => node.sources = self.parse_bracketed_identifiers()?,
                "quorum" => node.quorum = self.parse_optional_int(),
                "timeout" => {
                    let t = self.current().clone();
                    match t.ttype {
                        TokenType::Duration | TokenType::StringLit => {
                            self.advance();
                            node.timeout = t.value;
                        }
                        _ => node.timeout = self.consume_any_ident_or_kw()?.value,
                    }
                }
                "on_partition" => {
                    let p_tok = self.consume_any_ident_or_kw()?;
                    let p = p_tok.value;
                    if !matches!(p.as_str(), "fail" | "shield_quarantine") {
                        return Err(ParseError {
                            message: format!(
                                "Invalid on_partition '{p}' in observe '{}' — \
                                 expected fail | shield_quarantine",
                                node.name
                            ),
                            line: p_tok.line,
                            column: p_tok.column,
                                                    ..Default::default()
                        });
                    }
                    node.on_partition = p;
                }
                "certainty_floor" => node.certainty_floor = self.parse_optional_float(),
                _ => self.skip_value(),
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    // ── §λ-L-E Fase 3 — Control cognitivo ─────────────────────────

    /// Parse: `reconcile Name { observe, threshold, tolerance, on_drift, shield, mandate, max_retries }`.
    fn parse_reconcile(&mut self) -> Result<ReconcileDefinition, ParseError> {
        let tok = self.consume(TokenType::Reconcile)?;
        let name = self.consume(TokenType::Identifier)?.value;
        let mut node = ReconcileDefinition {
            name,
            observe_ref: String::new(),
            threshold: None,
            tolerance: None,
            on_drift: "provision".to_string(),
            shield_ref: String::new(),
            mandate_ref: String::new(),
            max_retries: 3,
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };
        self.consume(TokenType::LBrace)?;
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let field_name = self.current().value.clone();
            self.advance();
            if !self.check(TokenType::Colon) {
                if self.check(TokenType::LBrace) {
                    self.skip_braced_block()?;
                }
                continue;
            }
            self.advance();
            match field_name.as_str() {
                "observe" => node.observe_ref = self.consume_any_ident_or_kw()?.value,
                "threshold" => node.threshold = self.parse_optional_float(),
                "tolerance" => node.tolerance = self.parse_optional_float(),
                "on_drift" => {
                    let d_tok = self.consume_any_ident_or_kw()?;
                    let d = d_tok.value;
                    if !matches!(d.as_str(), "provision" | "alert" | "refine") {
                        return Err(ParseError {
                            message: format!(
                                "Invalid on_drift '{d}' in reconcile '{}' — \
                                 expected provision | alert | refine",
                                node.name
                            ),
                            line: d_tok.line,
                            column: d_tok.column,
                                                    ..Default::default()
                        });
                    }
                    node.on_drift = d;
                }
                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
                "mandate" => node.mandate_ref = self.consume_any_ident_or_kw()?.value,
                "max_retries" => {
                    if let Some(v) = self.parse_optional_int() {
                        node.max_retries = v;
                    }
                }
                _ => self.skip_value(),
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    /// Parse: `lease Name { resource, duration, acquire, on_expire }`.
    fn parse_lease(&mut self) -> Result<LeaseDefinition, ParseError> {
        let tok = self.consume(TokenType::Lease)?;
        let name = self.consume(TokenType::Identifier)?.value;
        let mut node = LeaseDefinition {
            name,
            resource_ref: String::new(),
            duration: String::new(),
            acquire: "on_start".to_string(),
            on_expire: "anchor_breach".to_string(),
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };
        self.consume(TokenType::LBrace)?;
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let field_name = self.current().value.clone();
            self.advance();
            if !self.check(TokenType::Colon) {
                if self.check(TokenType::LBrace) {
                    self.skip_braced_block()?;
                }
                continue;
            }
            self.advance();
            match field_name.as_str() {
                "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
                "duration" => {
                    let t = self.current().clone();
                    match t.ttype {
                        TokenType::Duration | TokenType::StringLit => {
                            self.advance();
                            node.duration = t.value;
                        }
                        _ => node.duration = self.consume_any_ident_or_kw()?.value,
                    }
                }
                "acquire" => {
                    let a_tok = self.consume_any_ident_or_kw()?;
                    let a = a_tok.value;
                    if !matches!(a.as_str(), "on_start" | "on_demand") {
                        return Err(ParseError {
                            message: format!(
                                "Invalid acquire '{a}' in lease '{}' — \
                                 expected on_start | on_demand",
                                node.name
                            ),
                            line: a_tok.line,
                            column: a_tok.column,
                                                    ..Default::default()
                        });
                    }
                    node.acquire = a;
                }
                "on_expire" => {
                    let e_tok = self.consume_any_ident_or_kw()?;
                    let e = e_tok.value;
                    if !matches!(e.as_str(), "anchor_breach" | "release" | "extend") {
                        return Err(ParseError {
                            message: format!(
                                "Invalid on_expire '{e}' in lease '{}' — \
                                 expected anchor_breach | release | extend",
                                node.name
                            ),
                            line: e_tok.line,
                            column: e_tok.column,
                                                    ..Default::default()
                        });
                    }
                    node.on_expire = e;
                }
                _ => self.skip_value(),
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    /// Parse: `ensemble Name { observations, quorum, aggregation, certainty_mode }`.
    fn parse_ensemble(&mut self) -> Result<EnsembleDefinition, ParseError> {
        let tok = self.consume(TokenType::Ensemble)?;
        let name = self.consume(TokenType::Identifier)?.value;
        let mut node = EnsembleDefinition {
            name,
            observations: Vec::new(),
            quorum: None,
            aggregation: "majority".to_string(),
            certainty_mode: "min".to_string(),
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };
        self.consume(TokenType::LBrace)?;
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let field_name = self.current().value.clone();
            self.advance();
            if !self.check(TokenType::Colon) {
                if self.check(TokenType::LBrace) {
                    self.skip_braced_block()?;
                }
                continue;
            }
            self.advance();
            match field_name.as_str() {
                "observations" => node.observations = self.parse_bracketed_identifiers()?,
                "quorum" => node.quorum = self.parse_optional_int(),
                "aggregation" => {
                    let a_tok = self.consume_any_ident_or_kw()?;
                    let a = a_tok.value;
                    if !matches!(a.as_str(), "majority" | "weighted" | "byzantine") {
                        return Err(ParseError {
                            message: format!(
                                "Invalid aggregation '{a}' in ensemble '{}' — \
                                 expected majority | weighted | byzantine",
                                node.name
                            ),
                            line: a_tok.line,
                            column: a_tok.column,
                                                    ..Default::default()
                        });
                    }
                    node.aggregation = a;
                }
                "certainty_mode" => {
                    let c_tok = self.consume_any_ident_or_kw()?;
                    let c = c_tok.value;
                    if !matches!(c.as_str(), "min" | "weighted" | "harmonic") {
                        return Err(ParseError {
                            message: format!(
                                "Invalid certainty_mode '{c}' in ensemble '{}' — \
                                 expected min | weighted | harmonic",
                                node.name
                            ),
                            line: c_tok.line,
                            column: c_tok.column,
                                                    ..Default::default()
                        });
                    }
                    node.certainty_mode = c;
                }
                _ => self.skip_value(),
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    // ── §λ-L-E Fase 4 — Topology + π-calculus binary sessions ─────

    /// Parse: `session Name { role1: [step, …]  role2: [step, …] }`.
    ///
    /// The enclosing `parse_session_definition` disambiguates from the session
    /// step token `session` (which does not exist) by always entering from the
    /// top-level dispatch; the identifier role name is consumed after `{`.
    fn parse_session_definition(&mut self) -> Result<SessionDefinition, ParseError> {
        let tok = self.consume(TokenType::Session)?;
        let name = self.consume(TokenType::Identifier)?.value;
        let mut node = SessionDefinition {
            name,
            roles: Vec::new(),
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };
        self.consume(TokenType::LBrace)?;
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let role_tok = self.consume_any_ident_or_kw()?;
            self.consume(TokenType::Colon)?;
            let steps = self.parse_session_steps()?;
            node.roles.push(SessionRole {
                name: role_tok.value,
                steps,
                loc: Loc {
                    line: role_tok.line,
                    column: role_tok.column,
                },
            });
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    /// §Fase 41.b — Parse:
    /// `socket Name { protocol: SessionRef, backpressure: credit(n),
    ///               reconnect: cognitive_state, legal_basis: ... }`.
    /// Fields are `key: value` pairs (order-free); only `protocol` is required.
    fn parse_socket(&mut self) -> Result<SocketDefinition, ParseError> {
        let tok = self.consume(TokenType::Socket)?;
        let name = self.consume(TokenType::Identifier)?.value;
        let mut node = SocketDefinition {
            name,
            loc: Loc { line: tok.line, column: tok.column },
            ..Default::default()
        };
        self.consume(TokenType::LBrace)?;
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let key = self.consume_any_ident_or_kw()?.value;
            self.consume(TokenType::Colon)?;
            match key.as_str() {
                "protocol" => node.protocol = self.consume_any_ident_or_kw()?.value,
                "backpressure" => {
                    // `credit(n)` — the typed-resource window.
                    let kind = self.consume_any_ident_or_kw()?.value;
                    if kind != "credit" {
                        return Err(self.error(&format!("expected `credit(n)` for backpressure, got `{kind}`")));
                    }
                    self.consume(TokenType::LParen)?;
                    let n = self
                        .consume(TokenType::Integer)?
                        .value
                        .parse::<i64>()
                        .map_err(|_| self.error("backpressure credit must be an integer"))?;
                    self.consume(TokenType::RParen)?;
                    node.backpressure_credit = Some(n);
                }
                "reconnect" => {
                    let mode = self.consume_any_ident_or_kw()?.value;
                    node.reconnect = mode == "cognitive_state";
                }
                "legal_basis" => node.legal_basis = Some(self.consume_any_ident_or_kw()?.value),
                other => return Err(self.error(&format!("unknown socket field `{other}`"))),
            }
            // Optional comma between fields.
            if self.check(TokenType::Comma) {
                self.consume(TokenType::Comma)?;
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    /// Parse: `[send T, receive U, loop, end]`.
    fn parse_session_steps(&mut self) -> Result<Vec<SessionStep>, ParseError> {
        self.consume(TokenType::LBracket)?;
        let mut steps = Vec::new();
        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
            steps.push(self.parse_session_step()?);
            if self.check(TokenType::Comma) {
                self.advance();
            }
        }
        self.consume(TokenType::RBracket)?;
        Ok(steps)
    }

    fn parse_session_step(&mut self) -> Result<SessionStep, ParseError> {
        let tok = self.current().clone();
        let loc = Loc { line: tok.line, column: tok.column };
        match tok.ttype {
            TokenType::Send => {
                self.advance();
                let msg = self.consume_any_ident_or_kw()?;
                Ok(SessionStep { op: "send".into(), message_type: msg.value, loc, ..Default::default() })
            }
            TokenType::Receive => {
                self.advance();
                let msg = self.consume_any_ident_or_kw()?;
                Ok(SessionStep { op: "receive".into(), message_type: msg.value, loc, ..Default::default() })
            }
            TokenType::Loop => {
                self.advance();
                Ok(SessionStep { op: "loop".into(), loc, ..Default::default() })
            }
            TokenType::End => {
                self.advance();
                Ok(SessionStep { op: "end".into(), loc, ..Default::default() })
            }
            // §Fase 41.b — choice: `select { ℓ: [..], … }` (⊕) | `branch { ℓ: [..], … }` (&).
            // `select`/`branch` are not keywords — they arrive as identifiers.
            TokenType::Identifier if tok.value == "select" || tok.value == "branch" => {
                self.parse_session_choice(&tok.value, loc)
            }
            _ => Err(ParseError {
                message: format!(
                    "Invalid session step '{}' — expected send | receive | loop | end | select | branch",
                    tok.value
                ),
                line: tok.line,
                column: tok.column,
                ..Default::default()
            }),
        }
    }

    /// §Fase 41.b — Parse a choice step: `select { ask: [..], cancel: [..] }`
    /// (or `branch { … }`). Each `label: [steps]` arm is a nested sub-protocol.
    fn parse_session_choice(&mut self, op: &str, loc: Loc) -> Result<SessionStep, ParseError> {
        self.advance(); // consume `select` / `branch`
        self.consume(TokenType::LBrace)?;
        let mut branches = Vec::new();
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let label_tok = self.consume_any_ident_or_kw()?;
            self.consume(TokenType::Colon)?;
            let steps = self.parse_session_steps()?;
            branches.push(SessionBranch {
                label: label_tok.value,
                steps,
                loc: Loc { line: label_tok.line, column: label_tok.column },
            });
            if self.check(TokenType::Comma) {
                self.advance();
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(SessionStep { op: op.to_string(), branches, loc, ..Default::default() })
    }

    /// Parse: `topology Name { nodes: [A, B, …]  edges: [A -> B : Session, …] }`.
    fn parse_topology(&mut self) -> Result<TopologyDefinition, ParseError> {
        let tok = self.consume(TokenType::Topology)?;
        let name = self.consume(TokenType::Identifier)?.value;
        let mut node = TopologyDefinition {
            name,
            nodes: Vec::new(),
            edges: Vec::new(),
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };
        self.consume(TokenType::LBrace)?;
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let field_name = self.current().value.clone();
            self.advance();
            if !self.check(TokenType::Colon) {
                if self.check(TokenType::LBrace) {
                    self.skip_braced_block()?;
                }
                continue;
            }
            self.advance();
            match field_name.as_str() {
                "nodes" => node.nodes = self.parse_bracketed_identifiers()?,
                "edges" => node.edges = self.parse_topology_edges()?,
                _ => self.skip_value(),
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    fn parse_topology_edges(&mut self) -> Result<Vec<TopologyEdge>, ParseError> {
        self.consume(TokenType::LBracket)?;
        let mut edges = Vec::new();
        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
            edges.push(self.parse_topology_edge()?);
            if self.check(TokenType::Comma) {
                self.advance();
            }
        }
        self.consume(TokenType::RBracket)?;
        Ok(edges)
    }

    fn parse_topology_edge(&mut self) -> Result<TopologyEdge, ParseError> {
        let src_tok = self.consume_any_ident_or_kw()?;
        self.consume(TokenType::Arrow)?;
        let tgt_tok = self.consume_any_ident_or_kw()?;
        self.consume(TokenType::Colon)?;
        let sess_tok = self.consume_any_ident_or_kw()?;
        Ok(TopologyEdge {
            source: src_tok.value,
            target: tgt_tok.value,
            session_ref: sess_tok.value,
            loc: Loc {
                line: src_tok.line,
                column: src_tok.column,
            },
        })
    }

    // ── §λ-L-E Fase 5 — Cognitive immune system (paper_immune_v2.md) ────

    /// Parse: `immune Name { watch, sensitivity, baseline, window, scope, tau, decay }`.
    fn parse_immune(&mut self) -> Result<ImmuneDefinition, ParseError> {
        let tok = self.consume(TokenType::Immune)?;
        let name = self.consume(TokenType::Identifier)?.value;
        let mut node = ImmuneDefinition {
            name,
            watch: Vec::new(),
            sensitivity: None,
            baseline: "learned".to_string(),
            window: 100,
            scope: String::new(),
            tau: String::new(),
            decay: "exponential".to_string(),
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };
        self.consume(TokenType::LBrace)?;
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let field_name = self.current().value.clone();
            self.advance();
            if !self.check(TokenType::Colon) {
                if self.check(TokenType::LBrace) {
                    self.skip_braced_block()?;
                }
                continue;
            }
            self.advance();
            match field_name.as_str() {
                "watch" => node.watch = self.parse_bracketed_identifiers()?,
                "sensitivity" => node.sensitivity = self.parse_optional_float(),
                "baseline" => node.baseline = self.consume_any_ident_or_kw()?.value,
                "window" => {
                    if let Some(v) = self.parse_optional_int() {
                        node.window = v;
                    }
                }
                "scope" => {
                    let s_tok = self.consume_any_ident_or_kw()?;
                    let s = s_tok.value;
                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
                        return Err(ParseError {
                            message: format!(
                                "Invalid scope '{s}' in immune '{}' — \
                                 expected tenant | flow | global",
                                node.name
                            ),
                            line: s_tok.line,
                            column: s_tok.column,
                                                    ..Default::default()
                        });
                    }
                    node.scope = s;
                }
                "tau" => {
                    let t = self.current().clone();
                    match t.ttype {
                        TokenType::Duration | TokenType::StringLit => {
                            self.advance();
                            node.tau = t.value;
                        }
                        _ => node.tau = self.consume_any_ident_or_kw()?.value,
                    }
                }
                "decay" => {
                    let d_tok = self.consume_any_ident_or_kw()?;
                    let d = d_tok.value;
                    if !matches!(d.as_str(), "exponential" | "linear" | "none") {
                        return Err(ParseError {
                            message: format!(
                                "Invalid decay '{d}' in immune '{}' — \
                                 expected exponential | linear | none",
                                node.name
                            ),
                            line: d_tok.line,
                            column: d_tok.column,
                                                    ..Default::default()
                        });
                    }
                    node.decay = d;
                }
                _ => self.skip_value(),
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    /// Parse: `reflex Name { trigger, on_level, action, scope, sla }`.
    fn parse_reflex(&mut self) -> Result<ReflexDefinition, ParseError> {
        let tok = self.consume(TokenType::Reflex)?;
        let name = self.consume(TokenType::Identifier)?.value;
        let mut node = ReflexDefinition {
            name,
            trigger: String::new(),
            on_level: "doubt".to_string(),
            action: String::new(),
            scope: String::new(),
            sla: String::new(),
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };
        self.consume(TokenType::LBrace)?;
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let field_name = self.current().value.clone();
            self.advance();
            if !self.check(TokenType::Colon) {
                if self.check(TokenType::LBrace) {
                    self.skip_braced_block()?;
                }
                continue;
            }
            self.advance();
            match field_name.as_str() {
                "trigger" => node.trigger = self.consume_any_ident_or_kw()?.value,
                "on_level" => {
                    let l_tok = self.consume_any_ident_or_kw()?;
                    let l = l_tok.value;
                    if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
                        return Err(ParseError {
                            message: format!(
                                "Invalid on_level '{l}' in reflex '{}' — \
                                 expected know | believe | speculate | doubt",
                                node.name
                            ),
                            line: l_tok.line,
                            column: l_tok.column,
                                                    ..Default::default()
                        });
                    }
                    node.on_level = l;
                }
                "action" => {
                    let a_tok = self.consume_any_ident_or_kw()?;
                    let a = a_tok.value;
                    if !matches!(
                        a.as_str(),
                        "drop"
                            | "revoke"
                            | "emit"
                            | "redact"
                            | "quarantine"
                            | "terminate"
                            | "alert"
                    ) {
                        return Err(ParseError {
                            message: format!(
                                "Invalid action '{a}' in reflex '{}' — \
                                 expected drop | revoke | emit | redact | \
                                 quarantine | terminate | alert",
                                node.name
                            ),
                            line: a_tok.line,
                            column: a_tok.column,
                                                    ..Default::default()
                        });
                    }
                    node.action = a;
                }
                "scope" => {
                    let s_tok = self.consume_any_ident_or_kw()?;
                    let s = s_tok.value;
                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
                        return Err(ParseError {
                            message: format!(
                                "Invalid scope '{s}' in reflex '{}' — \
                                 expected tenant | flow | global",
                                node.name
                            ),
                            line: s_tok.line,
                            column: s_tok.column,
                                                    ..Default::default()
                        });
                    }
                    node.scope = s;
                }
                "sla" => {
                    let t = self.current().clone();
                    match t.ttype {
                        TokenType::Duration | TokenType::StringLit => {
                            self.advance();
                            node.sla = t.value;
                        }
                        _ => node.sla = self.consume_any_ident_or_kw()?.value,
                    }
                }
                _ => self.skip_value(),
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    /// Parse: `heal Name { source, on_level, mode, scope, review_sla, shield, max_patches }`.
    fn parse_heal(&mut self) -> Result<HealDefinition, ParseError> {
        let tok = self.consume(TokenType::Heal)?;
        let name = self.consume(TokenType::Identifier)?.value;
        let mut node = HealDefinition {
            name,
            source: String::new(),
            on_level: "doubt".to_string(),
            mode: "human_in_loop".to_string(),
            scope: String::new(),
            review_sla: String::new(),
            shield_ref: String::new(),
            max_patches: 3,
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };
        self.consume(TokenType::LBrace)?;
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let field_name = self.current().value.clone();
            self.advance();
            if !self.check(TokenType::Colon) {
                if self.check(TokenType::LBrace) {
                    self.skip_braced_block()?;
                }
                continue;
            }
            self.advance();
            match field_name.as_str() {
                "source" => node.source = self.consume_any_ident_or_kw()?.value,
                "on_level" => {
                    let l_tok = self.consume_any_ident_or_kw()?;
                    let l = l_tok.value;
                    if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
                        return Err(ParseError {
                            message: format!(
                                "Invalid on_level '{l}' in heal '{}' — \
                                 expected know | believe | speculate | doubt",
                                node.name
                            ),
                            line: l_tok.line,
                            column: l_tok.column,
                                                    ..Default::default()
                        });
                    }
                    node.on_level = l;
                }
                "mode" => {
                    let m_tok = self.consume_any_ident_or_kw()?;
                    let m = m_tok.value;
                    if !matches!(m.as_str(), "audit_only" | "human_in_loop" | "adversarial") {
                        return Err(ParseError {
                            message: format!(
                                "Invalid mode '{m}' in heal '{}' — \
                                 expected audit_only | human_in_loop | adversarial",
                                node.name
                            ),
                            line: m_tok.line,
                            column: m_tok.column,
                                                    ..Default::default()
                        });
                    }
                    node.mode = m;
                }
                "scope" => {
                    let s_tok = self.consume_any_ident_or_kw()?;
                    let s = s_tok.value;
                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
                        return Err(ParseError {
                            message: format!(
                                "Invalid scope '{s}' in heal '{}' — \
                                 expected tenant | flow | global",
                                node.name
                            ),
                            line: s_tok.line,
                            column: s_tok.column,
                                                    ..Default::default()
                        });
                    }
                    node.scope = s;
                }
                "review_sla" => {
                    let t = self.current().clone();
                    match t.ttype {
                        TokenType::Duration | TokenType::StringLit => {
                            self.advance();
                            node.review_sla = t.value;
                        }
                        _ => node.review_sla = self.consume_any_ident_or_kw()?.value,
                    }
                }
                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
                "max_patches" => {
                    if let Some(v) = self.parse_optional_int() {
                        node.max_patches = v;
                    }
                }
                _ => self.skip_value(),
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    // ── §λ-L-E Fase 9 — UI cognitiva (component / view) ────────────

    /// Parse: `component Name { renders, via_shield, on_interact, render_hint }`.
    fn parse_component(&mut self) -> Result<ComponentDefinition, ParseError> {
        let tok = self.consume(TokenType::Component)?;
        let name = self.consume(TokenType::Identifier)?.value;
        let mut node = ComponentDefinition {
            name,
            renders: String::new(),
            via_shield: String::new(),
            on_interact: String::new(),
            render_hint: "custom".to_string(),
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };
        self.consume(TokenType::LBrace)?;
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let field_name = self.current().value.clone();
            self.advance();
            if !self.check(TokenType::Colon) {
                if self.check(TokenType::LBrace) {
                    self.skip_braced_block()?;
                }
                continue;
            }
            self.advance();
            match field_name.as_str() {
                "renders" => node.renders = self.consume_any_ident_or_kw()?.value,
                "via_shield" => node.via_shield = self.consume_any_ident_or_kw()?.value,
                "on_interact" => node.on_interact = self.consume_any_ident_or_kw()?.value,
                "render_hint" => {
                    let h_tok = self.consume_any_ident_or_kw()?;
                    let h = h_tok.value;
                    if !matches!(h.as_str(), "card" | "list" | "form" | "chart" | "custom") {
                        return Err(ParseError {
                            message: format!(
                                "Invalid render_hint '{h}' in component '{}' — \
                                 expected card | list | form | chart | custom",
                                node.name
                            ),
                            line: h_tok.line,
                            column: h_tok.column,
                                                    ..Default::default()
                        });
                    }
                    node.render_hint = h;
                }
                _ => self.skip_value(),
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    /// Parse: `view Name { title, components: [...], route }`.
    fn parse_view(&mut self) -> Result<ViewDefinition, ParseError> {
        let tok = self.consume(TokenType::View)?;
        let name = self.consume(TokenType::Identifier)?.value;
        let mut node = ViewDefinition {
            name,
            title: String::new(),
            components: Vec::new(),
            route: String::new(),
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };
        self.consume(TokenType::LBrace)?;
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let field_name = self.current().value.clone();
            self.advance();
            if !self.check(TokenType::Colon) {
                if self.check(TokenType::LBrace) {
                    self.skip_braced_block()?;
                }
                continue;
            }
            self.advance();
            match field_name.as_str() {
                "title" => node.title = self.consume(TokenType::StringLit)?.value,
                "components" => node.components = self.parse_bracketed_identifiers()?,
                "route" => node.route = self.consume(TokenType::StringLit)?.value,
                _ => self.skip_value(),
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    fn parse_axonendpoint(&mut self) -> Result<AxonEndpointDefinition, ParseError> {
        let tok = self.consume(TokenType::AxonEndpoint)?;
        let name = self.consume(TokenType::Identifier)?.value;
        let mut node = AxonEndpointDefinition {
            name,
            method: String::new(),
            path: String::new(),
            body_type: String::new(),
            execute_flow: String::new(),
            output_type: String::new(),
            shield_ref: String::new(),
            retries: None,
            timeout: String::new(),
            compliance: Vec::new(),
            // §Fase 30 — Defaults preserve backwards compat per D1.
            transport: "json".to_string(),
            keepalive: String::new(),
            // §Fase 31.b — Inference fields (parser-default state).
            // Both fields toggle/populate only when the source provides
            // an explicit `transport:` declaration (parser sets
            // `transport_explicit = true`) AND the type-checker walks
            // the program to compute `implicit_transport`.
            transport_explicit: false,
            implicit_transport: String::new(),
            // §Fase 32.g (D8) — auth scope; empty list ≡ no auth gate.
            requires_capabilities: Vec::new(),
            // §Fase 32.h — Replay-token binding (D9 plan-vivo).
            // Parser defaults: not explicit; effective value resolved
            // at deploy time using the method-default heuristic.
            replay_explicit: false,
            replay: false,
            // §Fase 33.z.k.b (v1.28.0) — Wire-format dialect default
            // empty; the runtime classifier resolves the default
            // dialect per the algebraic-effect predicate when the
            // source omits `transport: sse(<dialect>)`.
            transport_dialect: String::new(),
            // §Fase 33.z.k.1 (v1.27.1) — Algebraic-effect override.
            // Parser default false; populated by the type-checker's
            // compute_implicit_transports pass once the full program
            // is known (the predicate cross-references tool effects
            // declared anywhere in the program).
            has_algebraic_stream_effect: false,
            // §Fase 36.d (D2) — declared execution backend; empty ≡
            // not declared (the endpoint resolves down the Fase 36 D1
            // ladder). A non-empty value is validated against the
            // closed `AXONENDPOINT_BACKEND_VALUES` catalog below.
            backend: String::new(),
            // §Fase 37.y (D1) — Path-param names extracted from the
            // `path:` string AFTER the field is parsed. Initialized
            // empty; populated by `extract_path_param_names` after
            // the `path:` field is read in the loop below.
            path_params: Vec::new(),
            // §Fase 37.y (D2) — Inline `query: { name: Type, name: Type? }`
            // block. Initialized empty; populated by the `"query"` arm
            // in the field loop below. Closed catalog enforced at parse
            // time per `axonendpoint_is_valid_query_param_type`.
            query_params: Vec::new(),
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };
        self.consume(TokenType::LBrace)?;
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let field_name = self.current().value.clone();
            self.advance();
            if self.check(TokenType::Colon) {
                self.advance();
                match field_name.as_str() {
                    "method" => {
                        // §Fase 32.b D3 — closed method enum
                        // `{GET, POST, PUT, DELETE, PATCH}`. Unknown
                        // values rejected at parse time with smart-
                        // suggest hint (Fase 28.e). HEAD/OPTIONS/etc.
                        // are runtime-managed and not adopter-
                        // declarable.
                        let value_tok = self.consume_any_ident_or_kw()?;
                        let value_upper = value_tok.value.to_uppercase();
                        if !axonendpoint_is_valid_method(&value_upper) {
                            let hint = crate::smart_suggest::suggest_for(
                                &value_upper,
                                AXONENDPOINT_METHOD_VALUES,
                            );
                            let base = format!(
                                "Invalid method '{}' in axonendpoint '{}'.",
                                value_tok.value, node.name
                            );
                            let message = if hint.is_empty() {
                                format!(
                                    "{base} expected GET | POST | PUT | DELETE | PATCH, found {}",
                                    value_tok.value
                                )
                            } else {
                                format!(
                                    "{base} {hint} (expected GET | POST | PUT | DELETE | PATCH, found {})",
                                    value_tok.value
                                )
                            };
                            return Err(ParseError {
                                message,
                                line: value_tok.line,
                                column: value_tok.column,
                                ..Default::default()
                            });
                        }
                        node.method = value_upper;
                    }
                    "path" => {
                        node.path = self.consume(TokenType::StringLit)?.value.clone();
                        // §Fase 37.y (D1) — extract `{name}` placeholders
                        // for the Request Binding Contract's path-param
                        // source. Duplicate `{name}` in the same path
                        // is rejected at parse time (HTTP route patterns
                        // structurally reject duplicates; surfacing the
                        // error here is friendlier than letting axum
                        // panic at registration).
                        match extract_path_param_names(&node.path) {
                            Ok(names) => node.path_params = names,
                            Err(dup) => {
                                let cur = self.current().clone();
                                return Err(ParseError {
                                    message: format!(
                                        "axonendpoint '{}' declares path '{}' \
                                         containing duplicate placeholder '{{{}}}'. \
                                         Each `{{name}}` in a `path:` must be \
                                         unique — the runtime cannot bind two \
                                         path segments to the same name (Fase 37.y D1).",
                                        node.name, node.path, dup,
                                    ),
                                    line: cur.line,
                                    column: cur.column,
                                    ..Default::default()
                                });
                            }
                        }
                    },
                    "body" => node.body_type = self.consume_any_ident_or_kw()?.value.clone(),
                    "query" => {
                        // §Fase 37.y (D2) — Inline query-parameter block.
                        // Grammar: `query: { name: Type [, name: Type?]* }`.
                        // Closed type catalog
                        // `AXONENDPOINT_QUERY_PARAM_TYPES = {Text, Int,
                        // Float, Bool, Uuid}`. Optional via `?` suffix
                        // reuses `TypeExpr.optional` semantics already in
                        // use for flow parameters + body type fields. A
                        // duplicate field name in the same block is a
                        // parse error (HTTP query strings DO allow
                        // multi-value but v1.38.5 binds the first value
                        // only — see plan vivo §7 forward-compat).
                        //
                        // §Fase 37.y (D2 robustness) — declaring `query:`
                        // twice on the same axonendpoint silently merged
                        // params pre-hardening. Now it's a parse error
                        // so an adopter typo / copy-paste mistake
                        // surfaces with line + column instead of
                        // producing an unexpectedly-augmented endpoint.
                        let lbrace_tok = self.consume(TokenType::LBrace)?;
                        let block_line = lbrace_tok.line;
                        if !node.query_params.is_empty() {
                            return Err(ParseError {
                                message: format!(
                                    "axonendpoint '{}' declares `query: {{ … }}` \
                                     more than once. The query-parameter block \
                                     is unique per endpoint; combine all params \
                                     into a single block (Fase 37.y D2).",
                                    node.name,
                                ),
                                line: lbrace_tok.line,
                                column: lbrace_tok.column,
                                ..Default::default()
                            });
                        }
                        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
                            let name_tok = self.consume(TokenType::Identifier)?;
                            let field_name = name_tok.value.clone();
                            // Duplicate detection within the block.
                            if node
                                .query_params
                                .iter()
                                .any(|f| f.name == field_name)
                            {
                                return Err(ParseError {
                                    message: format!(
                                        "axonendpoint '{}' declares duplicate \
                                         query param '{}' inside `query: {{ … }}`. \
                                         Each name must appear at most once \
                                         (Fase 37.y D2).",
                                        node.name, field_name,
                                    ),
                                    line: name_tok.line,
                                    column: name_tok.column,
                                    ..Default::default()
                                });
                            }
                            self.consume(TokenType::Colon)?;
                            let type_expr = self.parse_type_expr()?;
                            // §Fase 37.y (D2 robustness) — reject generic
                            // type expressions on query params. The
                            // closed catalog is 5 primitives; container
                            // types (`Optional<T>`, `List<T>`, etc.)
                            // would mislead the adopter into thinking
                            // they bind multi-value query strings
                            // (deferred per plan vivo §7) or that
                            // `Optional<Text>` is the canonical way to
                            // declare an optional query (it's NOT —
                            // `Text?` is). Surface the canonical syntax
                            // verbatim so the fix is obvious.
                            if !type_expr.generic_param.is_empty() {
                                let canonical_hint = if type_expr.name == "Optional" {
                                    format!(
                                        " Use `{}?` (the `?` suffix) for an \
                                         optional query param instead of \
                                         `Optional<{}>`.",
                                        type_expr.generic_param,
                                        type_expr.generic_param,
                                    )
                                } else if type_expr.name == "List" {
                                    " Multi-value query params (e.g. `?tag=a&tag=b`) \
                                     are honest-deferred from v1.38.5; bind a \
                                     single-value `Text` query param and parse \
                                     the value inside the flow."
                                        .to_string()
                                } else {
                                    String::new()
                                };
                                return Err(ParseError {
                                    message: format!(
                                        "axonendpoint '{}' query param '{}' uses \
                                         a generic type `{}<{}>`. Query params \
                                         take a primitive type from the closed \
                                         catalog ({}); the `?` suffix marks \
                                         optional.{} (Fase 37.y D2).",
                                        node.name,
                                        field_name,
                                        type_expr.name,
                                        type_expr.generic_param,
                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | "),
                                        canonical_hint,
                                    ),
                                    line: type_expr.loc.line,
                                    column: type_expr.loc.column,
                                    ..Default::default()
                                });
                            }
                            // Validate against the closed catalog. A
                            // miss surfaces a Fase 28-style smart-suggest
                            // hint when within edit-distance 2.
                            if !axonendpoint_is_valid_query_param_type(&type_expr.name) {
                                // `smart_suggest::suggest_for` returns
                                // pre-formatted prose like
                                // "Did you mean `Text`?" or
                                // "Did you mean `Text` or `Int`?" (empty
                                // when no candidate within edit-distance
                                // 2). Concatenate without re-wrapping.
                                let hint = crate::smart_suggest::suggest_for(
                                    &type_expr.name,
                                    AXONENDPOINT_QUERY_PARAM_TYPES,
                                );
                                let hint_text = if hint.is_empty() {
                                    format!(
                                        " Expected one of: {}.",
                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | ")
                                    )
                                } else {
                                    format!(
                                        " {} Expected one of: {}.",
                                        hint,
                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | ")
                                    )
                                };
                                return Err(ParseError {
                                    message: format!(
                                        "axonendpoint '{}' query param '{}' has \
                                         unsupported type '{}'.{} (Fase 37.y D2).",
                                        node.name, field_name, type_expr.name,
                                        hint_text,
                                    ),
                                    line: type_expr.loc.line,
                                    column: type_expr.loc.column,
                                    ..Default::default()
                                });
                            }
                            node.query_params.push(TypeField {
                                name: field_name,
                                type_expr,
                                loc: Loc {
                                    line: name_tok.line,
                                    column: name_tok.column,
                                },
                            });
                            // Trailing comma is optional; the next loop
                            // iteration handles `}` cleanly. Accept both
                            // `name: Type, name: Type` AND `name: Type
                            // name: Type` (the existing parser style is
                            // forgiving about list separators).
                            if self.check(TokenType::Comma) {
                                self.advance();
                            }
                            let _ = block_line; // suppress unused warning
                        }
                        self.consume(TokenType::RBrace)?;
                    },
                    "execute" => node.execute_flow = self.consume_any_ident_or_kw()?.value.clone(),
                    "output" => {
                        // §Fase 38.x.f — promote axonendpoint `output:`
                        // parsing from a single token to the full
                        // generic-aware type expression (mirroring
                        // `parse_step` for FlowStep::Step which already
                        // uses `parse_output_type_string`).
                        //
                        // Pre-38.x.f: `output: List<Item>` captured only
                        // `"List"`, dropping `<Item>` (next tokens were
                        // either left unconsumed or absorbed by the
                        // following field). v1.39.0's narrow cardinality
                        // gate happened to fire correctly for `output: T`
                        // + retrieve-tail because the singular-detection
                        // path used `!starts_with("List<")` — but the
                        // SYMMETRIC `output: List<T>` + singular-tail
                        // case (38.x.f D3) needs the FULL `List<T>`
                        // shape captured; without it the gate sees
                        // `"List"` and misclassifies as Singular.
                        node.output_type = self.parse_output_type_string()?;
                    }
                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
                    "retries" => node.retries = self.parse_optional_int(),
                    "timeout" => {
                        let t = self.current().clone();
                        self.advance();
                        node.timeout = t.value.clone();
                    }
                    "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
                    "replay" => {
                        // §Fase 32.h (D9 plan-vivo) — Replay-token binding.
                        // Boolean `replay: true | false`. Default (when
                        // omitted) is method-derived at deploy-time:
                        // POST/PUT → true, GET/DELETE → false. Explicit
                        // declaration sets `replay_explicit = true` so
                        // the runtime knows NOT to override.
                        let value_tok = self.consume(TokenType::Bool)?;
                        node.replay = value_tok.value.eq_ignore_ascii_case("true");
                        node.replay_explicit = true;
                    }
                    "requires" => {
                        // §Fase 32.g (D8) — Auth scope per axonendpoint.
                        // Closed slug grammar
                        // `^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$` enforced
                        // at parse time with smart-suggest-style hint.
                        // Empty list means "no auth gate" (D9 backwards-
                        // compat). Cross-stack with Python parser.
                        let bracket_tok = self.current().clone();
                        let items = self.parse_bracketed_dot_identifiers()?;
                        for slug in &items {
                            if !is_valid_capability_slug(slug) {
                                return Err(ParseError {
                                    message: format!(
                                        "Invalid capability slug '{slug}' in axonendpoint '{}' \
                                         `requires:`. Capability slugs must match \
                                         ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
                                         lowercase identifiers starting with a letter. Examples: \
                                         `admin`, `legal.read`, `hipaa.phi.read`.",
                                        node.name
                                    ),
                                    line: bracket_tok.line,
                                    column: bracket_tok.column,
                                    ..Default::default()
                                });
                            }
                        }
                        node.requires_capabilities = items;
                    }
                    // §Fase 30.b — HTTP transport enum (D2 closed) + keepalive (D6 closed).
                    // Mirrors `axon/compiler/parser.py` `_parse_axonendpoint`.
                    // Drift-gate corpus verifies byte-identical parse cross-stack.
                    "transport" => {
                        let value_tok = self.consume_any_ident_or_kw()?;
                        let value = &value_tok.value;
                        if !axonendpoint_is_valid_transport(value) {
                            let hint = crate::smart_suggest::suggest_for(
                                value,
                                AXONENDPOINT_TRANSPORT_VALUES,
                            );
                            let base = format!(
                                "Invalid transport '{}' in axonendpoint '{}'.",
                                value, node.name
                            );
                            let message = if hint.is_empty() {
                                format!("{base} expected json | sse | ndjson, found {value}")
                            } else {
                                format!(
                                    "{base} {hint} (expected json | sse | ndjson, found {value})"
                                )
                            };
                            return Err(ParseError {
                                message,
                                line: value_tok.line,
                                column: value_tok.column,
                                ..Default::default()
                            });
                        }
                        node.transport = value.clone();
                        // §Fase 31.b D1 — mark the field as explicitly
                        // declared so the type-checker's implicit-transport
                        // inference knows NOT to override this value with
                        // the produces_stream-driven inference.
                        node.transport_explicit = true;
                        // §Fase 33.z.k.b (v1.28.0) — Optional dialect
                        // parametrization: `transport: sse(<dialect>)`.
                        // Only valid when the base value is `sse`
                        // (json + ndjson dialects are the dialects
                        // themselves; `json(<x>)` / `ndjson(<x>)`
                        // would be parse errors caught below).
                        if self.check(TokenType::LParen) {
                            if value != "sse" {
                                let tok = self.current().clone();
                                return Err(ParseError {
                                    message: format!(
                                        "Dialect parametrization \
                                         `transport: {value}(<dialect>)` is \
                                         only valid for `sse`; got \
                                         `{value}` in axonendpoint '{}'.",
                                        node.name
                                    ),
                                    line: tok.line,
                                    column: tok.column,
                                    ..Default::default()
                                });
                            }
                            self.advance(); // consume LParen
                            let dialect_tok = self.consume_any_ident_or_kw()?;
                            let dialect = dialect_tok.value.clone();
                            if !AXONENDPOINT_TRANSPORT_DIALECTS
                                .iter()
                                .any(|&d| d == dialect)
                            {
                                let hint = crate::smart_suggest::suggest_for(
                                    &dialect,
                                    AXONENDPOINT_TRANSPORT_DIALECTS,
                                );
                                let base = format!(
                                    "Invalid SSE dialect '{dialect}' in axonendpoint '{}'.",
                                    node.name
                                );
                                let message = if hint.is_empty() {
                                    format!(
                                        "{base} expected axon | openai | kimi | glm | anthropic, found {dialect}"
                                    )
                                } else {
                                    format!(
                                        "{base} {hint} (expected axon | openai | kimi | glm | anthropic, found {dialect})"
                                    )
                                };
                                return Err(ParseError {
                                    message,
                                    line: dialect_tok.line,
                                    column: dialect_tok.column,
                                    ..Default::default()
                                });
                            }
                            // Closing RParen.
                            let rparen_tok = self.current().clone();
                            if !self.check(TokenType::RParen) {
                                return Err(ParseError {
                                    message: format!(
                                        "Expected `)` after dialect name \
                                         in axonendpoint '{}' \
                                         (transport: sse(<dialect>) grammar).",
                                        node.name
                                    ),
                                    line: rparen_tok.line,
                                    column: rparen_tok.column,
                                    ..Default::default()
                                });
                            }
                            self.advance(); // consume RParen
                            node.transport_dialect = dialect;
                        }
                    }
                    "keepalive" => {
                        // Accepts either a DURATION token (e.g. `15s`) or
                        // an ident-like token. Validation against the
                        // closed enum {5s, 15s, 30s, 60s} happens after.
                        let value_tok = self.current().clone();
                        self.advance();
                        let value = &value_tok.value;
                        if !axonendpoint_is_valid_keepalive(value) {
                            let hint = crate::smart_suggest::suggest_for(
                                value,
                                AXONENDPOINT_KEEPALIVE_VALUES,
                            );
                            let base = format!(
                                "Invalid keepalive '{}' in axonendpoint '{}'.",
                                value, node.name
                            );
                            let message = if hint.is_empty() {
                                format!("{base} expected 5s | 15s | 30s | 60s, found {value}")
                            } else {
                                format!(
                                    "{base} {hint} (expected 5s | 15s | 30s | 60s, found {value})"
                                )
                            };
                            return Err(ParseError {
                                message,
                                line: value_tok.line,
                                column: value_tok.column,
                                ..Default::default()
                            });
                        }
                        node.keepalive = value.clone();
                    }
                    "backend" => {
                        // §Fase 36.d (D2) — declared execution backend.
                        // Closed catalog `CANONICAL_PROVIDERS ∪ {auto,
                        // stub}`; an unknown name is a parse error with
                        // a smart-suggest hint (the same discipline as
                        // `method`/`transport`/`keepalive`). The
                        // type-checker re-validates defensively for
                        // ASTs built outside the parser (LSP, tests).
                        let value_tok = self.consume_any_ident_or_kw()?;
                        let value = &value_tok.value;
                        if !axonendpoint_is_valid_backend(value) {
                            let hint = crate::smart_suggest::suggest_for(
                                value,
                                AXONENDPOINT_BACKEND_VALUES,
                            );
                            let expected = AXONENDPOINT_BACKEND_VALUES.join(" | ");
                            let base = format!(
                                "Invalid backend '{}' in axonendpoint '{}'.",
                                value, node.name
                            );
                            let message = if hint.is_empty() {
                                format!("{base} expected {expected}, found {value}")
                            } else {
                                format!(
                                    "{base} {hint} (expected {expected}, found {value})"
                                )
                            };
                            return Err(ParseError {
                                message,
                                line: value_tok.line,
                                column: value_tok.column,
                                ..Default::default()
                            });
                        }
                        node.backend = value.clone();
                    }
                    _ => self.skip_value(),
                }
            } else if self.check(TokenType::LBrace) {
                self.skip_braced_block()?;
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    // ── Numeric helpers for Tier 2 field parsing ────────────────────

    fn parse_optional_int(&mut self) -> Option<i64> {
        let tok = self.current().clone();
        match tok.ttype {
            TokenType::Integer => {
                self.advance();
                tok.value.parse::<i64>().ok()
            }
            _ => {
                self.advance();
                None
            }
        }
    }

    fn parse_optional_float(&mut self) -> Option<f64> {
        let tok = self.current().clone();
        match tok.ttype {
            TokenType::Float | TokenType::Integer => {
                self.advance();
                tok.value.parse::<f64>().ok()
            }
            _ => {
                self.advance();
                None
            }
        }
    }

    // ── LAMBDA DATA (ΛD) ──────────────────────────────────────────

    fn parse_lambda_data(&mut self) -> Result<LambdaDataDefinition, ParseError> {
        let tok = self.consume(TokenType::Lambda)?;
        let name = self.consume(TokenType::Identifier)?;
        self.consume(TokenType::LBrace)?;

        let mut node = LambdaDataDefinition {
            name: name.value.clone(),
            ontology: String::new(),
            certainty: 1.0,
            temporal_frame_start: String::new(),
            temporal_frame_end: String::new(),
            provenance: String::new(),
            derivation: String::new(),
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };

        while !self.check(TokenType::RBrace) {
            let field = self.current().clone();
            match field.ttype {
                TokenType::Ontology => {
                    self.advance();
                    self.consume(TokenType::Colon)?;
                    node.ontology = self.consume(TokenType::StringLit)?.value.clone();
                }
                TokenType::Certainty => {
                    self.advance();
                    self.consume(TokenType::Colon)?;
                    let val = self.current().clone();
                    match val.ttype {
                        TokenType::Float => {
                            self.advance();
                            node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
                        }
                        TokenType::Integer => {
                            self.advance();
                            node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
                        }
                        _ => {
                            return Err(ParseError {
                                message: format!(
                                    "Expected number for certainty, got '{}'",
                                    val.value
                                ),
                                line: val.line,
                                column: val.column,
                                                            ..Default::default()
                            });
                        }
                    }
                }
                TokenType::TemporalFrame => {
                    self.advance();
                    self.consume(TokenType::Colon)?;
                    node.temporal_frame_start = self.consume(TokenType::StringLit)?.value.clone();
                    // Optional second string for end frame
                    if self.check(TokenType::StringLit) {
                        node.temporal_frame_end = self.consume(TokenType::StringLit)?.value.clone();
                    }
                }
                TokenType::Provenance => {
                    self.advance();
                    self.consume(TokenType::Colon)?;
                    node.provenance = self.consume(TokenType::StringLit)?.value.clone();
                }
                TokenType::Derivation => {
                    self.advance();
                    self.consume(TokenType::Colon)?;
                    let d = self.current().clone();
                    self.advance();
                    node.derivation = d.value.clone();
                }
                _ => {
                    // Skip unknown fields gracefully
                    self.advance();
                    if self.check(TokenType::Colon) {
                        self.advance();
                        self.skip_value();
                    }
                }
            }
        }

        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    fn parse_lambda_data_apply(&mut self) -> Result<LambdaDataApplyNode, ParseError> {
        let tok = self.consume(TokenType::Lambda)?;
        let lambda_name = self.consume(TokenType::Identifier)?;

        // Expect "on" keyword (parsed as identifier since it's not reserved)
        let on_tok = self.current().clone();
        self.advance();
        if on_tok.value != "on" {
            return Err(ParseError {
                message: format!(
                    "Expected 'on' after lambda data name in flow step, got '{}'",
                    on_tok.value
                ),
                line: on_tok.line,
                column: on_tok.column,
                            ..Default::default()
            });
        }

        let target = self.current().clone();
        self.advance();

        let mut output_type = String::new();
        if self.check(TokenType::Arrow) {
            self.advance();
            output_type = self.consume(TokenType::Identifier)?.value.clone();
        }

        Ok(LambdaDataApplyNode {
            lambda_data_name: lambda_name.value.clone(),
            target: target.value.clone(),
            output_type,
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
        })
    }

    // ── GENERIC (Tier 2+) ────────────────────────────────────────

    fn parse_generic_declaration(&mut self) -> Result<Declaration, ParseError> {
        let kw_tok = self.current().clone();
        self.advance(); // consume keyword

        // Try to consume a name (identifier or keyword-as-name)
        let name = if self.current().ttype == TokenType::Identifier {
            let n = self.current().value.clone();
            self.advance();
            n
        } else if !self.check(TokenType::LBrace)
            && !self.check(TokenType::LParen)
            && !self.check(TokenType::Eof)
            && self
                .current()
                .value
                .chars()
                .all(|c| c.is_alphanumeric() || c == '_')
        {
            let n = self.current().value.clone();
            self.advance();
            n
        } else {
            String::new()
        };

        // Skip optional parens: (...)
        if self.check(TokenType::LParen) {
            self.advance();
            let mut depth = 1u32;
            while depth > 0 && !self.check(TokenType::Eof) {
                if self.check(TokenType::LParen) {
                    depth += 1;
                } else if self.check(TokenType::RParen) {
                    depth -= 1;
                }
                self.advance();
            }
        }

        // Skip tokens until LBrace or next declaration
        while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
            if self.check(TokenType::Eof) {
                break;
            }
            self.advance();
        }

        // Skip braced block if present
        if self.check(TokenType::LBrace) {
            self.skip_braced_block()?;
        }

        Ok(Declaration::Generic(GenericDeclaration {
            keyword: kw_tok.value,
            name,
            loc: Loc {
                line: kw_tok.line,
                column: kw_tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        }))
    }

    // ──────────────────────────────────────────────────────────────────
    //  §λ-L-E Fase 13 — Mobile Typed Channels parsers
    //  (paper_mobile_channels.md §3 + plan/fase_13)
    //  Direct port of axon/compiler/parser.py:_parse_channel/emit/publish/discover.
    // ──────────────────────────────────────────────────────────────────

    /// Parse: `channel Name { message, qos, lifetime, persistence, shield }`.
    fn parse_channel(&mut self) -> Result<ChannelDefinition, ParseError> {
        let tok = self.consume(TokenType::Channel)?;
        let name = self.consume(TokenType::Identifier)?.value;
        let mut node = ChannelDefinition {
            name: name.clone(),
            message: String::new(),
            qos: "at_least_once".to_string(),
            lifetime: "affine".to_string(),
            persistence: "ephemeral".to_string(),
            shield_ref: String::new(),
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
            leading_trivia: Vec::new(),
            trailing_trivia: Vec::new(),
        };
        self.consume(TokenType::LBrace)?;
        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
            let field_tok = self.current().clone();
            let field_name = field_tok.value.clone();
            self.advance();
            if !self.check(TokenType::Colon) {
                if self.check(TokenType::LBrace) {
                    self.skip_braced_block()?;
                }
                continue;
            }
            self.advance();
            match field_name.as_str() {
                "message" => node.message = self.parse_channel_message_type()?,
                "qos" => {
                    let q_tok = self.consume_any_ident_or_kw()?;
                    if !matches!(
                        q_tok.value.as_str(),
                        "at_most_once" | "at_least_once" | "exactly_once" | "broadcast" | "queue"
                    ) {
                        return Err(ParseError {
                            message: format!(
                                "Invalid qos '{}' in channel '{}' — \
                                 expected at_most_once | at_least_once | \
                                 exactly_once | broadcast | queue",
                                q_tok.value, name
                            ),
                            line: q_tok.line,
                            column: q_tok.column,
                                                    ..Default::default()
                        });
                    }
                    node.qos = q_tok.value;
                }
                "lifetime" => {
                    let lt_tok = self.consume_any_ident_or_kw()?;
                    if !matches!(lt_tok.value.as_str(), "linear" | "affine" | "persistent") {
                        return Err(ParseError {
                            message: format!(
                                "Invalid lifetime '{}' in channel '{}' — \
                                 expected linear | affine | persistent",
                                lt_tok.value, name
                            ),
                            line: lt_tok.line,
                            column: lt_tok.column,
                                                    ..Default::default()
                        });
                    }
                    node.lifetime = lt_tok.value;
                }
                "persistence" => {
                    let p_tok = self.consume_any_ident_or_kw()?;
                    if !matches!(p_tok.value.as_str(), "ephemeral" | "persistent_axonstore") {
                        return Err(ParseError {
                            message: format!(
                                "Invalid persistence '{}' in channel '{}' — \
                                 expected ephemeral | persistent_axonstore",
                                p_tok.value, name
                            ),
                            line: p_tok.line,
                            column: p_tok.column,
                                                    ..Default::default()
                        });
                    }
                    node.persistence = p_tok.value;
                }
                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
                _ => self.skip_value(),
            }
        }
        self.consume(TokenType::RBrace)?;
        Ok(node)
    }

    /// Parse a `message:` value, supporting nested `Channel<…>`
    /// (second-order session types — paper §3.3).
    fn parse_channel_message_type(&mut self) -> Result<String, ParseError> {
        let head = self.consume(TokenType::Identifier)?;
        let mut spelling = head.value;
        if self.check(TokenType::Lt) {
            self.advance();
            let inner = self.parse_channel_message_type()?;
            self.consume(TokenType::Gt)?;
            spelling = format!("{}<{}>", spelling, inner);
        }
        Ok(spelling)
    }

    /// Parse: `emit ChannelName(value_ref)` — Chan-Output / Chan-Mobility.
    ///
    /// `value_ref` accepts a bare identifier (variable / channel name for
    /// mobility) or a dotted path (`Step.output.field`) referencing a prior
    /// step result (Fase 13.i — runtime resolves via ContextManager).
    fn parse_emit_step(&mut self) -> Result<FlowStep, ParseError> {
        let tok = self.consume(TokenType::Emit)?;
        let channel = self.consume(TokenType::Identifier)?.value;
        self.consume(TokenType::LParen)?;
        let value = self.parse_emit_value_ref()?;
        self.consume(TokenType::RParen)?;
        Ok(FlowStep::Emit(EmitStatement {
            channel_ref: channel,
            value_ref: value,
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
        }))
    }

    /// Parse: `IDENTIFIER ('.' (IDENTIFIER | keyword))*` → dot-joined string
    /// (Fase 13.i).
    ///
    /// Mirrors the Python `_parse_emit_value_ref` helper exactly so the IR
    /// JSON for `emit Hello(Build.output)` is byte-identical between the
    /// two reference implementations.
    ///
    /// The HEAD must be a real ``Identifier``. Subsequent segments after a
    /// `.` may be identifiers OR keywords — common field names like
    /// ``output``, ``result``, ``message``, ``state``, etc. are reserved
    /// words in Axon but adopters must be able to write them as
    /// dotted-access segments. The accepting predicate:
    ///   - the lexer carried a non-empty `value` (every Word-like token does)
    ///   - the value's first byte is a letter or underscore (filters out
    ///     punctuation tokens such as ',', '{', etc.)
    fn parse_emit_value_ref(&mut self) -> Result<String, ParseError> {
        let head = self.consume(TokenType::Identifier)?.value;
        let mut parts = vec![head];
        while self.check(TokenType::Dot) {
            self.advance(); // consume '.'
            let next_tok = self.current().clone();
            let valid = !next_tok.value.is_empty()
                && next_tok.value.as_bytes()[0].is_ascii_alphabetic()
                || next_tok.value.starts_with('_');
            if !valid {
                return Err(ParseError {
                    message: format!(
                        "Expected identifier or keyword after '.' in dotted \
                         access, found {:?}",
                        next_tok.value
                    ),
                    line: next_tok.line,
                    column: next_tok.column,
                                    ..Default::default()
                });
            }
            self.advance();
            parts.push(next_tok.value);
        }
        Ok(parts.join("."))
    }

    /// Parse: `publish ChannelName within ShieldName` — Publish-Ext (D8).
    fn parse_publish_step(&mut self) -> Result<FlowStep, ParseError> {
        let tok = self.consume(TokenType::Publish)?;
        let channel = self.consume(TokenType::Identifier)?.value;
        self.consume(TokenType::Within)?;
        let shield = self.consume(TokenType::Identifier)?.value;
        Ok(FlowStep::Publish(PublishStatement {
            channel_ref: channel,
            shield_ref: shield,
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
        }))
    }

    /// Parse: `discover ChannelName as alias` — dual of publish.
    fn parse_discover_step(&mut self) -> Result<FlowStep, ParseError> {
        let tok = self.consume(TokenType::Discover)?;
        let cap = self.consume(TokenType::Identifier)?.value;
        self.consume(TokenType::As)?;
        let alias = self.consume(TokenType::Identifier)?.value;
        Ok(FlowStep::Discover(DiscoverStatement {
            capability_ref: cap,
            alias,
            loc: Loc {
                line: tok.line,
                column: tok.column,
            },
        }))
    }
}

// ── §λ-L-E Fase 13 — Mobile Typed Channels parser tests ─────────────────────

#[cfg(test)]
mod fase13_parser_tests {
    use super::*;
    use crate::lexer::Lexer;

    fn parse(src: &str) -> Result<Program, ParseError> {
        let tokens = Lexer::new(src, "<test>").tokenize().expect("lex");
        Parser::new(tokens).parse()
    }

    #[test]
    fn channel_full_parses() {
        let src = r#"channel C { message: Order qos: at_least_once lifetime: affine persistence: ephemeral shield: Gate }"#;
        let prog = parse(src).expect("parse");
        match &prog.declarations[0] {
            Declaration::Channel(c) => {
                assert_eq!(c.name, "C");
                assert_eq!(c.message, "Order");
                assert_eq!(c.qos, "at_least_once");
                assert_eq!(c.lifetime, "affine");
                assert_eq!(c.persistence, "ephemeral");
                assert_eq!(c.shield_ref, "Gate");
            }
            _ => panic!("expected ChannelDefinition"),
        }
    }

    #[test]
    fn channel_defaults_match_paper_d1() {
        let prog = parse("channel C { message: Order }").expect("parse");
        if let Declaration::Channel(c) = &prog.declarations[0] {
            assert_eq!(c.qos, "at_least_once"); // default
            assert_eq!(c.lifetime, "affine"); // D1 default
            assert_eq!(c.persistence, "ephemeral");
            assert_eq!(c.shield_ref, "");
        } else {
            panic!("expected ChannelDefinition");
        }
    }

    #[test]
    fn channel_second_order_message_type_parses() {
        let prog = parse("channel C { message: Channel<Order> }").expect("parse");
        if let Declaration::Channel(c) = &prog.declarations[0] {
            assert_eq!(c.message, "Channel<Order>");
        } else {
            panic!("expected ChannelDefinition");
        }
    }

    #[test]
    fn channel_nested_channel_message_type_parses() {
        let prog = parse("channel C { message: Channel<Channel<Order>> }").expect("parse");
        if let Declaration::Channel(c) = &prog.declarations[0] {
            assert_eq!(c.message, "Channel<Channel<Order>>");
        } else {
            panic!("expected ChannelDefinition");
        }
    }

    #[test]
    fn channel_invalid_qos_rejected() {
        let err = parse("channel C { message: T qos: bogus }").unwrap_err();
        assert!(err.message.contains("Invalid qos"), "got {}", err.message);
    }

    #[test]
    fn channel_invalid_lifetime_rejected() {
        let err = parse("channel C { message: T lifetime: eternal }").unwrap_err();
        assert!(
            err.message.contains("Invalid lifetime"),
            "got {}",
            err.message
        );
    }

    #[test]
    fn channel_invalid_persistence_rejected() {
        let err = parse("channel C { message: T persistence: forever }").unwrap_err();
        assert!(
            err.message.contains("Invalid persistence"),
            "got {}",
            err.message
        );
    }

    #[test]
    fn emit_value_parses() {
        let src = "flow f() -> Out { emit C(payload) }";
        let prog = parse(src).expect("parse");
        if let Declaration::Flow(f) = &prog.declarations[0] {
            match &f.body[0] {
                FlowStep::Emit(e) => {
                    assert_eq!(e.channel_ref, "C");
                    assert_eq!(e.value_ref, "payload");
                }
                other => panic!("expected Emit, got {:?}", other),
            }
        } else {
            panic!("expected Flow");
        }
    }

    #[test]
    fn publish_within_shield_parses() {
        let src = "flow f() -> Cap { publish C within Gate }";
        let prog = parse(src).expect("parse");
        if let Declaration::Flow(f) = &prog.declarations[0] {
            match &f.body[0] {
                FlowStep::Publish(p) => {
                    assert_eq!(p.channel_ref, "C");
                    assert_eq!(p.shield_ref, "Gate");
                }
                other => panic!("expected Publish, got {:?}", other),
            }
        } else {
            panic!("expected Flow");
        }
    }

    #[test]
    fn discover_with_alias_parses() {
        let src = "flow f() -> Out { discover C as ch }";
        let prog = parse(src).expect("parse");
        if let Declaration::Flow(f) = &prog.declarations[0] {
            match &f.body[0] {
                FlowStep::Discover(d) => {
                    assert_eq!(d.capability_ref, "C");
                    assert_eq!(d.alias, "ch");
                }
                other => panic!("expected Discover, got {:?}", other),
            }
        } else {
            panic!("expected Flow");
        }
    }

    #[test]
    fn listen_typed_ref_sets_flag_true() {
        let src = "daemon D() { goal: \"x\" listen C as ev { } }";
        let prog = parse(src).expect("parse");
        if let Declaration::Daemon(d) = &prog.declarations[0] {
            assert_eq!(d.listeners.len(), 1);
            assert_eq!(d.listeners[0].channel, "C");
            assert!(d.listeners[0].channel_is_ref, "typed ref ⇒ true");
        } else {
            panic!("expected Daemon");
        }
    }

    #[test]
    fn listen_string_topic_legacy_flag_false() {
        let src = "daemon D() { goal: \"x\" listen \"orders\" as ev { } }";
        let prog = parse(src).expect("parse");
        if let Declaration::Daemon(d) = &prog.declarations[0] {
            assert_eq!(d.listeners.len(), 1);
            assert_eq!(d.listeners[0].channel, "orders");
            assert!(!d.listeners[0].channel_is_ref, "string topic ⇒ false");
        } else {
            panic!("expected Daemon");
        }
    }

    // ── Fase 13.i — emit value_ref accepts dotted access ───────────

    fn extract_first_emit(prog: &Program) -> &EmitStatement {
        if let Declaration::Flow(f) = &prog.declarations[0] {
            if let FlowStep::Emit(e) = &f.body[0] {
                return e;
            }
        }
        panic!("expected emit statement at flow body[0]");
    }

    #[test]
    fn emit_accepts_bare_identifier_value_ref() {
        // Pre-13.i baseline — must keep working.
        let prog = parse("flow f() -> Out { emit Hello(payload) }").expect("parse");
        let emit = extract_first_emit(&prog);
        assert_eq!(emit.channel_ref, "Hello");
        assert_eq!(emit.value_ref, "payload");
    }

    #[test]
    fn emit_accepts_two_segment_dotted_value_ref() {
        // The exact case adopters reported as broken before 13.i.
        let prog = parse("flow f() -> Out { emit Hello(Build.output) }").expect("parse");
        let emit = extract_first_emit(&prog);
        assert_eq!(emit.value_ref, "Build.output");
    }

    #[test]
    fn emit_accepts_three_segment_nested_dotted_value_ref() {
        let prog = parse("flow f() -> Out { emit Score(Analyze.result.score) }").expect("parse");
        let emit = extract_first_emit(&prog);
        assert_eq!(emit.value_ref, "Analyze.result.score");
    }

    #[test]
    fn emit_dotted_with_trailing_dot_fails() {
        // Trailing `.` must still error — every '.' demands an identifier.
        let result = parse("flow f() -> Out { emit Hello(Build.) }");
        assert!(result.is_err(), "expected parse error for trailing dot");
    }
}

// ── §Fase 14.a — declaration_trivia parallel channel tests ──────────────────

#[cfg(test)]
mod fase14a_declaration_trivia_tests {
    use super::*;
    use crate::lexer::Lexer;
    use crate::tokens::TriviaKind;

    fn parse(src: &str) -> Program {
        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
        Parser::new(toks).parse().expect("parse")
    }

    #[test]
    fn no_comments_means_empty_trivia_per_decl() {
        let prog = parse("flow F() -> Out { }");
        assert_eq!(prog.declarations.len(), 1);
        assert_eq!(prog.declaration_trivia.len(), 1);
        assert!(prog.declaration_trivia[0].leading.is_empty());
        assert!(prog.declaration_trivia[0].trailing.is_empty());
    }

    #[test]
    fn doc_line_comment_attaches_as_leading() {
        let prog = parse("/// Documents F\nflow F() -> Out { }");
        let triv = &prog.declaration_trivia[0];
        assert_eq!(triv.leading.len(), 1);
        assert_eq!(triv.leading[0].kind, TriviaKind::DocLine);
        assert!(triv.leading[0].is_doc());
        assert_eq!(triv.leading[0].text, "/// Documents F");
    }

    #[test]
    fn regular_line_comment_attaches_as_leading() {
        let prog = parse("// header\nflow F() -> Out { }");
        let triv = &prog.declaration_trivia[0];
        assert_eq!(triv.leading.len(), 1);
        assert_eq!(triv.leading[0].kind, TriviaKind::Line);
        assert!(!triv.leading[0].is_doc());
    }

    #[test]
    fn block_doc_comment_attaches_as_leading() {
        let prog = parse("/** Doc block */\nflow F() -> Out { }");
        let triv = &prog.declaration_trivia[0];
        assert_eq!(triv.leading[0].kind, TriviaKind::DocBlock);
        assert!(triv.leading[0].is_doc());
    }

    #[test]
    fn multiple_comments_collected_in_source_order() {
        let src = "/// First\n/// Second\nflow F() -> Out { }";
        let prog = parse(src);
        let triv = &prog.declaration_trivia[0];
        assert_eq!(triv.leading.len(), 2);
        assert_eq!(triv.leading[0].text, "/// First");
        assert_eq!(triv.leading[1].text, "/// Second");
    }

    #[test]
    fn three_decls_each_get_own_leading() {
        let src = "/// for A\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }\n/// for C\nflow C() -> Out { }";
        let prog = parse(src);
        assert_eq!(prog.declarations.len(), 3);
        assert_eq!(prog.declaration_trivia.len(), 3);
        for (idx, name) in ["A", "B", "C"].iter().enumerate() {
            let triv = &prog.declaration_trivia[idx];
            assert_eq!(triv.leading.len(), 1);
            assert_eq!(triv.leading[0].text, format!("/// for {name}"));
        }
    }

    #[test]
    fn trailing_comment_attaches_to_last_token_of_decl() {
        // Comment on the same line as the decl's closing brace.
        let prog = parse("flow F() -> Out { } // tail");
        let triv = &prog.declaration_trivia[0];
        assert_eq!(triv.trailing.len(), 1);
        assert_eq!(triv.trailing[0].text, "// tail");
    }

    #[test]
    fn mixed_doc_and_regular_preserve_order_between_decls() {
        let src = "/// doc for A\nflow A() -> Out { }\n\n// header line\n/// doc for B\nflow B() -> Out { }";
        let prog = parse(src);
        assert_eq!(prog.declarations.len(), 2);
        // A: just the doc comment.
        assert_eq!(prog.declaration_trivia[0].leading.len(), 1);
        // B: header + doc, in source order.
        assert_eq!(prog.declaration_trivia[1].leading.len(), 2);
        assert_eq!(prog.declaration_trivia[1].leading[0].text, "// header line");
        assert_eq!(prog.declaration_trivia[1].leading[1].text, "/// doc for B");
    }

    #[test]
    fn parser_unaffected_by_comments_in_grammar_path() {
        // The parser must accept comments interleaved between every
        // legal token without affecting the AST shape it produces.
        // This is the regression guard for "lossless lexing must not
        // change parsing semantics."
        let src =
            "// before flow\nflow /* between flow and name */ F() -> Out {\n  // body comment\n}";
        let prog = parse(src);
        assert_eq!(prog.declarations.len(), 1);
        if let Declaration::Flow(f) = &prog.declarations[0] {
            assert_eq!(f.name, "F");
        } else {
            panic!("expected Flow declaration");
        }
    }
}

// ── §Fase 14.b — per-struct trivia fields tests ─────────────────────────────
//
// 14.b spreads `leading_trivia` / `trailing_trivia` into every Declaration
// variant struct (FlowDefinition, ChannelDefinition, PersonaDefinition, …).
// The Python AST already had this shape since 14.a; 14.b achieves Rust
// parity. The side-channel `Program.declaration_trivia` is preserved for
// backward compat — these tests verify the new direct access path.

#[cfg(test)]
mod fase14b_per_struct_trivia_tests {
    use super::*;
    use crate::lexer::Lexer;
    use crate::tokens::TriviaKind;

    fn parse(src: &str) -> Program {
        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
        Parser::new(toks).parse().expect("parse")
    }

    #[test]
    fn flow_definition_carries_leading_trivia_directly() {
        let prog = parse("/// documents F\nflow F() -> Out { }");
        if let Declaration::Flow(f) = &prog.declarations[0] {
            assert_eq!(f.leading_trivia.len(), 1);
            assert_eq!(f.leading_trivia[0].kind, TriviaKind::DocLine);
            assert_eq!(f.leading_trivia[0].text, "/// documents F");
            assert!(f.trailing_trivia.is_empty());
        } else {
            panic!("expected Flow declaration");
        }
    }

    #[test]
    fn flow_definition_carries_trailing_trivia_directly() {
        let prog = parse("flow F() -> Out { } // tail comment");
        if let Declaration::Flow(f) = &prog.declarations[0] {
            assert_eq!(f.trailing_trivia.len(), 1);
            assert_eq!(f.trailing_trivia[0].text, "// tail comment");
        } else {
            panic!("expected Flow declaration");
        }
    }

    #[test]
    fn channel_definition_carries_trivia_directly() {
        // ChannelDefinition is a Tier-1 declaration; verify per-struct fields
        // populate just like FlowDefinition.
        let src = concat!(
            "/// inbound order events\n",
            "channel Orders {\n",
            "    message:     Order\n",
            "    qos:         at_least_once\n",
            "    lifetime:    affine\n",
            "    persistence: ephemeral\n",
            "    shield:      Broker\n",
            "}",
        );
        let prog = parse(src);
        if let Declaration::Channel(ch) = &prog.declarations[0] {
            assert_eq!(ch.leading_trivia.len(), 1);
            assert!(ch.leading_trivia[0].is_doc());
            assert_eq!(ch.leading_trivia[0].text, "/// inbound order events");
        } else {
            panic!("expected Channel declaration");
        }
    }

    #[test]
    fn per_struct_fields_match_side_channel() {
        // 14.a side-channel and 14.b per-struct fields must hold identical
        // data — they are populated by the same parser pass.
        let src = "/// for A\n// header for B\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }";
        let prog = parse(src);
        for (idx, decl) in prog.declarations.iter().enumerate() {
            let side = &prog.declaration_trivia[idx];
            let (per_lead, per_trail) = match decl {
                Declaration::Flow(f) => (&f.leading_trivia, &f.trailing_trivia),
                _ => panic!("unexpected variant"),
            };
            assert_eq!(per_lead.len(), side.leading.len());
            assert_eq!(per_trail.len(), side.trailing.len());
            for (a, b) in per_lead.iter().zip(side.leading.iter()) {
                assert_eq!(a.text, b.text);
                assert_eq!(a.kind, b.kind);
            }
        }
    }

    #[test]
    fn comment_free_program_yields_empty_per_struct_fields() {
        let prog = parse("flow F() -> Out { }");
        if let Declaration::Flow(f) = &prog.declarations[0] {
            assert!(f.leading_trivia.is_empty());
            assert!(f.trailing_trivia.is_empty());
        } else {
            panic!("expected Flow declaration");
        }
    }
}

// ── §Fase 14.c — inner doc comments (//!, /*!) ──────────────────────────────
//
// Inner doc comments document the *enclosing* item rather than the next
// sibling. Today they flow through the trivia channel like any other
// comment; downstream consumers (axon doc, LSP) decide how to interpret
// `is_inner_doc()`. These tests verify the lexer→parser pipeline preserves
// the inner-doc discriminator end-to-end.

#[cfg(test)]
mod fase14c_inner_doc_tests {
    use super::*;
    use crate::lexer::Lexer;
    use crate::tokens::TriviaKind;

    fn parse(src: &str) -> Program {
        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
        Parser::new(toks).parse().expect("parse")
    }

    #[test]
    fn inner_doc_line_reaches_leading_trivia() {
        let src = "//! file-level docs\nflow F() -> Out { }";
        let prog = parse(src);
        let triv = &prog.declaration_trivia[0];
        assert_eq!(triv.leading.len(), 1);
        assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocLine);
        assert!(triv.leading[0].is_doc());
        assert!(triv.leading[0].is_inner_doc());
        assert_eq!(triv.leading[0].text, "//! file-level docs");
        assert_eq!(triv.leading[0].stripped_text(), " file-level docs");
    }

    #[test]
    fn inner_doc_block_reaches_leading_trivia() {
        let src = "/*! module-level docs */\nflow F() -> Out { }";
        let prog = parse(src);
        let triv = &prog.declaration_trivia[0];
        assert_eq!(triv.leading.len(), 1);
        assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocBlock);
        assert!(triv.leading[0].is_inner_doc());
        assert_eq!(triv.leading[0].stripped_text(), " module-level docs ");
    }

    #[test]
    fn outer_and_inner_doc_can_coexist() {
        // File-level inner doc on top, then an outer doc for the
        // declaration. Both reach the trivia channel and remain
        // distinguishable via `is_inner_doc()`.
        let src = "//! file docs\n/// docs F\nflow F() -> Out { }";
        let prog = parse(src);
        let triv = &prog.declaration_trivia[0];
        assert_eq!(triv.leading.len(), 2);
        assert!(triv.leading[0].is_inner_doc());
        assert!(triv.leading[1].is_doc());
        assert!(!triv.leading[1].is_inner_doc());
    }

    #[test]
    fn inner_doc_reaches_per_struct_fields() {
        // Same data must be visible via the per-struct fields (Fase 14.b).
        let src = "//! intro\nflow F() -> Out { }";
        let prog = parse(src);
        if let Declaration::Flow(f) = &prog.declarations[0] {
            assert_eq!(f.leading_trivia.len(), 1);
            assert!(f.leading_trivia[0].is_inner_doc());
        } else {
            panic!("expected Flow declaration");
        }
    }
}

// ── §Fase 28.c — Parser error recovery test pack ─────────────────────────────
//
// Mirror of `tests/test_fase28_parser_recovery.py` (Python side, 28.b).
// The test classes here line up 1-1 with the Python ones so the cross-
// stack drift gate (28.i) can compare error-list shapes input-for-input.
//
// Test classes:
//   - backwards_compat: existing `parse()` API unchanged
//   - single_error_recovery: one bad decl → one error, rest parse OK
//   - multi_error_recovery: N independent errors → N entries
//   - sync_points: every top-level keyword resyncs correctly
//   - parse_result_api: `has_errors`, `is_clean`
//   - edge_cases: EOF mid-error, brace imbalance, only-bad-tokens
//   - robustness_fuzz: 1000 deterministic-seeded mutations never crash
//   - no_ghost_errors: single broken field produces exactly 1 error
//   - integration_with_colon_diagnostic: v1.19.4 hint preserved under
//     recovery mode
#[cfg(test)]
mod fase28_recovery_tests {
    use super::*;
    use crate::lexer::Lexer;

    /// Lex a source and return tokens for the parser to consume.
    /// Mirrors the Python `_parse_recovery` helper.
    fn lex(src: &str) -> Vec<Token> {
        Lexer::new(src, "<test>").tokenize().expect("lex")
    }

    /// Parse with recovery mode. Returns `(program, errors)` so call
    /// sites read like the Python helper.
    fn recover(src: &str) -> ParseResult {
        Parser::new(lex(src)).parse_with_recovery()
    }

    /// Strict parse. Mirrors the Python `_parse_strict` helper.
    fn strict(src: &str) -> Result<Program, ParseError> {
        Parser::new(lex(src)).parse()
    }

    // ── backwards_compat ─────────────────────────────────────────

    #[test]
    fn strict_parse_unchanged_for_clean_source() {
        // The existing `parse()` API must continue to succeed
        // verbatim on every well-formed input — D9.
        let src = "intent I {}";
        let prog = strict(src).expect("clean parse");
        assert_eq!(prog.declarations.len(), 1);
    }

    #[test]
    fn strict_parse_still_raises_on_first_error() {
        // D9 + D8: opt-in to recovery via `parse_with_recovery`;
        // strict mode must still bubble the first error.
        // (Using a parse-time error rather than a lex error — `@@@`
        // would be rejected by the lexer, which is out of scope.)
        let src = "flow F() { } not_a_keyword flow G() { }";
        let _ = strict(src).expect_err("must error fast in strict mode");
    }

    #[test]
    fn recovery_clean_source_yields_no_errors() {
        let src = "flow F() { } flow G() { }";
        let pr = recover(src);
        assert!(pr.is_clean(), "errors: {:?}", pr.errors);
        assert_eq!(pr.program.declarations.len(), 2);
    }

    // ── single_error_recovery ────────────────────────────────────

    #[test]
    fn single_unknown_top_level_token_recovers() {
        // One garbage token at top level; rest must parse.
        let src = "garbage_token flow F() { } flow G() { }";
        let pr = recover(src);
        assert_eq!(pr.errors.len(), 1, "errors: {:?}", pr.errors);
        assert_eq!(pr.program.declarations.len(), 2);
    }

    #[test]
    fn error_in_first_decl_does_not_block_second() {
        // `flow F` body refers to non-keyword `nope`; the error
        // recovery must skip to the next top-level keyword.
        let src = "flow F() { not_a_step nope } flow G() { }";
        let pr = recover(src);
        assert!(pr.has_errors(), "expected at least one error");
        // The second flow must be reachable.
        let names: Vec<&str> = pr
            .program
            .declarations
            .iter()
            .filter_map(|d| match d {
                Declaration::Flow(f) => Some(f.name.as_str()),
                _ => None,
            })
            .collect();
        assert!(names.contains(&"G"), "G not found among {names:?}");
    }

    #[test]
    fn malformed_declaration_then_clean_intent_recovers() {
        let src = "flow @ () { } intent I {}";
        let pr = recover(src);
        assert!(pr.has_errors());
        let kinds: Vec<&str> = pr
            .program
            .declarations
            .iter()
            .map(|d| match d {
                Declaration::Intent(_) => "intent",
                Declaration::Flow(_) => "flow",
                _ => "other",
            })
            .collect();
        assert!(kinds.contains(&"intent"), "kinds: {kinds:?}");
    }

    #[test]
    fn recovery_does_not_double_count_a_single_error() {
        // Regression for the "ghost error" pathology that surfaced
        // during 28.b dev: a nested-decl error must not also fire
        // an "Unexpected token at top level" from the outer loop.
        // The Rust grammar has stricter intra-flow requirements
        // than Python; the invariant we assert here is that the
        // outer loop emits zero "Unexpected token at top level"
        // errors after an inner step-shape error.
        let src = "flow F() { not_a_step }";
        let pr = recover(src);
        let outer_ghosts = pr
            .errors
            .iter()
            .filter(|e| e.message.contains("at top level"))
            .count();
        assert_eq!(outer_ghosts, 0, "ghost errors: {:?}", pr.errors);
    }

    // ── multi_error_recovery ─────────────────────────────────────

    #[test]
    fn three_independent_errors_yield_three_entries() {
        let src =
            "garbage1 flow F() { } garbage2 flow G() { } garbage3 flow H() { }";
        let pr = recover(src);
        assert_eq!(pr.errors.len(), 3, "errors: {:?}", pr.errors);
        assert_eq!(pr.program.declarations.len(), 3);
    }

    #[test]
    fn all_errors_no_valid_declarations() {
        let src = "foo bar baz qux";
        let pr = recover(src);
        assert!(pr.has_errors());
        assert!(pr.program.declarations.is_empty());
    }

    #[test]
    fn errors_recorded_in_source_order() {
        let src = "x flow A() { } y flow B() { } z flow C() { }";
        let pr = recover(src);
        assert_eq!(pr.errors.len(), 3);
        let lines: Vec<u32> = pr.errors.iter().map(|e| e.line).collect();
        // Same source-line means we compare by column ordering;
        // either way they must be non-decreasing.
        assert!(
            lines.windows(2).all(|w| w[0] <= w[1]),
            "errors out of order: {lines:?}"
        );
    }

    // ── sync_points ──────────────────────────────────────────────

    #[test]
    fn sync_to_flow_keyword() {
        let src = "garbage flow F() { }";
        let pr = recover(src);
        assert_eq!(pr.program.declarations.len(), 1);
    }

    #[test]
    fn sync_to_intent_keyword() {
        let src = "garbage intent I {}";
        let pr = recover(src);
        assert_eq!(pr.program.declarations.len(), 1);
    }

    #[test]
    fn sync_to_persona_keyword() {
        let src = "garbage persona P { name: \"P\" role: \"R\" }";
        let pr = recover(src);
        assert!(
            pr.program
                .declarations
                .iter()
                .any(|d| matches!(d, Declaration::Persona(_))),
            "persona not recovered: decls = {:?}",
            pr.program.declarations.len()
        );
    }

    #[test]
    fn sync_to_run_keyword() {
        let src = "garbage run R { input: { user_message: \"hi\" } }";
        let pr = recover(src);
        // Either Run was parsed, or recovery still produced ≥1 err.
        assert!(pr.has_errors());
    }

    // ── parse_result_api ─────────────────────────────────────────

    #[test]
    fn parse_result_has_errors_and_is_clean_invert() {
        let pr_clean = recover("flow F() { }");
        assert!(pr_clean.is_clean());
        assert!(!pr_clean.has_errors());

        let pr_err = recover("garbage");
        assert!(!pr_err.is_clean());
        assert!(pr_err.has_errors());
    }

    #[test]
    fn parse_result_program_field_holds_partial_program() {
        let pr = recover("garbage flow F() { }");
        assert!(!pr.program.declarations.is_empty());
    }

    #[test]
    fn parse_result_errors_carry_line_and_column() {
        let pr = recover("garbage");
        assert!(!pr.errors.is_empty());
        let e = &pr.errors[0];
        assert!(e.line >= 1);
        // Column may be 0-based or 1-based depending on lexer;
        // accept anything ≥ 0.
        let _ = e.column;
        assert!(!e.message.is_empty());
    }

    #[test]
    fn parse_result_debug_renders() {
        let pr = recover("flow F() { }");
        let s = format!("{pr:?}");
        assert!(s.contains("ParseResult"));
    }

    // ── edge_cases ───────────────────────────────────────────────

    #[test]
    fn empty_source_is_clean() {
        let pr = recover("");
        assert!(pr.is_clean());
        assert!(pr.program.declarations.is_empty());
    }

    #[test]
    fn whitespace_only_source_is_clean() {
        let pr = recover("   \n\n\t  \n");
        assert!(pr.is_clean());
        assert!(pr.program.declarations.is_empty());
    }

    #[test]
    fn only_garbage_does_not_crash() {
        // Lex-clean garbage tokens (avoids AxonLexerError).
        let pr = recover("foo bar baz { qux quux } corge { grault }");
        assert!(pr.has_errors());
    }

    #[test]
    fn unbalanced_close_brace_does_not_crash() {
        let pr = recover("} flow F() { }");
        // Recovery must keep walking past stray `}`.
        let names: Vec<&str> = pr
            .program
            .declarations
            .iter()
            .filter_map(|d| match d {
                Declaration::Flow(f) => Some(f.name.as_str()),
                _ => None,
            })
            .collect();
        assert!(names.contains(&"F"), "F not recovered: {names:?}");
    }

    #[test]
    fn error_at_eof_does_not_loop() {
        // Truncated declaration. Must terminate; finite errors.
        let pr = recover("flow F() { ");
        // Either errored or somehow accepted — but must terminate.
        let _ = pr.errors.len();
    }

    #[test]
    fn nested_braces_inside_error_still_balance() {
        // Walker must respect brace depth so a `}` inside a malformed
        // block does not prematurely sync.
        let src = "flow F() { not_a_step { inner } } flow G() { }";
        let pr = recover(src);
        let names: Vec<&str> = pr
            .program
            .declarations
            .iter()
            .filter_map(|d| match d {
                Declaration::Flow(f) => Some(f.name.as_str()),
                _ => None,
            })
            .collect();
        assert!(names.contains(&"G"), "G not recovered: {names:?}");
    }

    // ── robustness_fuzz ──────────────────────────────────────────
    //
    // Deterministic-seeded mutator (xorshift). 100 buckets ×
    // 10 mutations = 1000 iterations, byte-bounded so fuzz time
    // stays under 1 s on a release build. Recovery must NEVER crash;
    // lexer-level errors are out of scope (lexer recovery is its own
    // sub-fase). 28.b mirrors this with the same structure.

    #[derive(Clone, Copy)]
    struct Xorshift(u64);
    impl Xorshift {
        fn next(&mut self) -> u64 {
            let mut x = self.0;
            x ^= x << 13;
            x ^= x >> 7;
            x ^= x << 17;
            self.0 = x;
            x
        }
        fn pick<T: Copy>(&mut self, slice: &[T]) -> T {
            slice[(self.next() as usize) % slice.len()]
        }
    }

    fn mutate(src: &str, rng: &mut Xorshift) -> String {
        let mut bytes: Vec<u8> = src.bytes().collect();
        if bytes.is_empty() {
            return src.to_string();
        }
        let op = rng.next() % 4;
        let pos = (rng.next() as usize) % bytes.len();
        // Stick to ASCII-safe printable bytes to keep input lex-able
        // most of the time. AxonLexerError is still possible and is
        // tolerated by the recovery contract.
        let safe: &[u8] = b"abcdefghijklmnopqrstuvwxyz {}();:,_0123456789";
        match op {
            0 => {
                bytes.remove(pos);
            }
            1 => {
                let b = rng.pick(safe);
                bytes.insert(pos, b);
            }
            2 if pos + 1 < bytes.len() => {
                bytes.swap(pos, pos + 1);
            }
            _ => {
                let b = rng.pick(safe);
                bytes[pos] = b;
            }
        }
        // Lossy decode: mutator may have produced invalid UTF-8;
        // strip non-ASCII before handing to the lexer.
        bytes.retain(|b| b.is_ascii());
        String::from_utf8_lossy(&bytes).into_owned()
    }

    #[test]
    fn fuzz_recovery_never_crashes() {
        let seed_bases = [
            "flow F() { }",
            "intent I { }",
            "persona P { name: \"P\" role: \"R\" }",
            "intent J { ask: \"a\" }",
            "type T = String",
        ];
        // 100 buckets × 10 mutations = 1000 iterations, deterministic.
        for (bucket, base) in (0..100u64).zip(seed_bases.iter().cycle()) {
            let mut rng = Xorshift(0x1234_5678_9abc_def0_u64.wrapping_add(bucket));
            let mut current = (*base).to_string();
            for _ in 0..10 {
                current = mutate(&current, &mut rng);
                // Lexer may reject; that's outside parser-recovery
                // scope (28.b/c). Skip those iterations.
                let toks = match Lexer::new(&current, "<fuzz>").tokenize() {
                    Ok(t) => t,
                    Err(_) => continue,
                };
                // Recovery must not panic on any well-lexed input.
                let _pr = Parser::new(toks).parse_with_recovery();
            }
        }
    }

    // ── integration_with_v1_19_4_colon_diagnostic ────────────────

    #[test]
    fn missing_colon_hint_preserved_under_recovery() {
        // The Rust frontend's strict `parse()` carries the same
        // colon diagnostic shape as the Python side. Recovery mode
        // must not erase it.
        let src = "flow F() { run R { input { user_message: \"hi\" } } }";
        let pr = recover(src);
        // Either the parser accepts this (some shape may be valid)
        // or it errors — but if it errors, the message must surface
        // the diagnostic content.
        if !pr.errors.is_empty() {
            let any_msg = pr.errors.iter().any(|e| !e.message.is_empty());
            assert!(any_msg);
        }
    }

    // ── recovery preserves declaration ordering ──────────────────

    #[test]
    fn recovered_declarations_appear_in_source_order() {
        let src = "flow A() { } garbage flow B() { } garbage flow C() { }";
        let pr = recover(src);
        let names: Vec<&str> = pr
            .program
            .declarations
            .iter()
            .filter_map(|d| match d {
                Declaration::Flow(f) => Some(f.name.as_str()),
                _ => None,
            })
            .collect();
        assert_eq!(names, vec!["A", "B", "C"]);
    }
}

// ── §Fase 28.d — Source-context diagnostic block test pack ───────────────────
//
// Mirror of `tests/test_fase28_source_context.py` (Python side, 28.d).
// The render output must be byte-identical to the Python `SourceSnippet.render`
// on the same input — D7 ratified (cross-stack drift gate). Golden strings
// in `golden_*` tests are duplicated verbatim in the Python pack; edits
// here MUST be mirrored on the Python side and vice versa.
#[cfg(test)]
mod fase28_source_context_tests {
    use super::*;
    use crate::lexer::Lexer;

    fn snippet(source: &str, line: u32, column: u32, filename: &str) -> String {
        SourceSnippet::new(
            source.to_string(),
            line,
            column,
            filename.to_string(),
        )
        .render()
    }

    // ── Pure rendering ──────────────────────────────────────────

    #[test]
    fn rustc_style_block_for_middle_line() {
        let src = "line one\nline two\nline three\nline four\nline five";
        let out = snippet(src, 3, 6, "x.axon");
        assert!(out.contains("--> x.axon:3:6"));
        assert!(out.contains("1 | line one"));
        assert!(out.contains("2 | line two"));
        assert!(out.contains("3 | line three"));
        assert!(out.contains("4 | line four"));
        assert!(out.contains("5 | line five"));
        // Caret col 6 → 5-space pad. Empty gutter is 1 space (gutter=1).
        assert!(out.contains("\n  |      ^"), "out:\n{out}");
    }

    #[test]
    fn caret_column_one_renders_correctly() {
        let out = snippet("abc\n", 1, 1, "<source>");
        assert!(out.contains("\n  | ^"));
    }

    #[test]
    fn first_line_clamps_context_before_to_zero() {
        let src = "first\nsecond\nthird\nfourth\nfifth";
        let out = snippet(src, 1, 1, "<source>");
        assert!(out.contains("1 | first"));
        assert!(out.contains("2 | second"));
        assert!(out.contains("3 | third"));
        assert!(!out.contains("4 | fourth"));
    }

    #[test]
    fn last_line_clamps_context_after_to_eof() {
        let src = "first\nsecond\nthird\nfourth\nfifth";
        let out = snippet(src, 5, 2, "<source>");
        assert!(out.contains("5 | fifth"));
        assert!(out.contains("3 | third"));
        assert!(out.contains("4 | fourth"));
        assert!(!out.contains("2 | second"));
    }

    #[test]
    fn gutter_width_grows_with_line_count() {
        let src: String = (1..=12).map(|i| format!("line{i}")).collect::<Vec<_>>().join("\n");
        let out = snippet(&src, 12, 1, "<source>");
        assert!(out.contains("12 | line12"));
        assert!(out.contains("10 | line10"));
    }

    // ── Edge cases ──────────────────────────────────────────────

    #[test]
    fn empty_source_returns_empty() {
        assert_eq!(snippet("", 1, 1, "<source>"), "");
    }

    #[test]
    fn zero_line_returns_empty() {
        assert_eq!(snippet("hi", 0, 1, "<source>"), "");
    }

    #[test]
    fn out_of_range_line_returns_empty() {
        assert_eq!(snippet("hi", 99, 1, "<source>"), "");
    }

    #[test]
    fn caret_clamps_past_eol() {
        let out = snippet("hello", 1, 50, "<source>");
        assert!(out.contains("\n  |      ^"), "out:\n{out}");
    }

    #[test]
    fn unicode_codepoint_count_for_caret_clamp() {
        // "héllo" = 5 codepoints; column past EOL clamps to 6.
        let out = snippet("héllo", 1, 99, "<source>");
        assert!(out.contains("\n  |      ^"), "out:\n{out}");
    }

    #[test]
    fn trailing_newline_does_not_create_phantom_last_line() {
        let out = snippet("first\nsecond\n", 2, 1, "<source>");
        assert!(!out.contains("3 |"));
        assert!(out.contains("2 | second"));
    }

    // ── Parser attach plumbing ──────────────────────────────────

    fn lex(src: &str) -> Vec<Token> {
        Lexer::new(src, "<test>").tokenize().expect("lex")
    }

    #[test]
    fn strict_parse_attaches_snippet_when_source_given() {
        let src = "garbage_token\nflow F() { }";
        let err = Parser::new(lex(src))
            .with_source(src, "x.axon")
            .parse()
            .expect_err("must error");
        assert!(err.source_snippet.is_some());
        let display = format!("{err}");
        assert!(display.contains("--> x.axon:"), "display: {display}");
    }

    #[test]
    fn strict_parse_no_snippet_when_no_source() {
        let src = "garbage_token";
        let err = Parser::new(lex(src)).parse().expect_err("must error");
        assert!(err.source_snippet.is_none());
        let display = format!("{err}");
        assert!(!display.contains("\n  -->"));
    }

    #[test]
    fn every_recovered_error_has_snippet() {
        let src = "garbage1\nflow F() { }\ngarbage2\nflow G() { }";
        let result = Parser::new(lex(src))
            .with_source(src, "multi.axon")
            .parse_with_recovery();
        assert!(!result.errors.is_empty());
        for err in &result.errors {
            assert!(err.source_snippet.is_some());
            let display = format!("{err}");
            assert!(
                display.contains("--> multi.axon:"),
                "display: {display}"
            );
        }
    }

    #[test]
    fn recovery_no_snippet_when_no_source() {
        let src = "garbage1 garbage2";
        let result = Parser::new(lex(src)).parse_with_recovery();
        for err in &result.errors {
            assert!(err.source_snippet.is_none());
        }
    }

    #[test]
    fn snippet_points_at_correct_line_for_each_error() {
        let src = "garbage_a\nflow F() { }\ngarbage_b\nflow G() { }";
        let result = Parser::new(lex(src))
            .with_source(src, "x")
            .parse_with_recovery();
        for err in &result.errors {
            let sn = err.source_snippet.as_ref().expect("snippet");
            assert_eq!(sn.line, err.line);
        }
    }

    // ── Backwards-compat ────────────────────────────────────────

    #[test]
    fn legacy_constructor_still_works() {
        let src = "flow F() { }";
        let prog = Parser::new(lex(src)).parse().expect("clean");
        assert_eq!(prog.declarations.len(), 1);
    }

    #[test]
    fn attach_source_idempotent() {
        let err = ParseError {
            message: "bad".to_string(),
            line: 2,
            column: 3,
            ..Default::default()
        };
        let err2 = err.clone().attach_source("a\nb\nc\n", "f.axon");
        let first = format!("{err2}");
        let err3 = err.attach_source("a\nb\nc\n", "f.axon");
        let second = format!("{err3}");
        assert_eq!(first, second);
    }

    #[test]
    fn attach_source_noop_when_line_zero() {
        let err = ParseError {
            message: "bad".to_string(),
            line: 0,
            column: 0,
            ..Default::default()
        };
        let err = err.attach_source("a\nb\nc\n", "f.axon");
        assert!(err.source_snippet.is_none());
    }

    // ── Cross-stack golden parity ───────────────────────────────
    // These golden strings are duplicated verbatim in the Python
    // test pack at `tests/test_fase28_source_context.py::TestRustParityShape`.
    // Edits here MUST be mirrored in the Python pack — D7.

    #[test]
    fn golden_simple_three_line_block() {
        let src = "alpha\nbeta\ngamma";
        let out = snippet(src, 2, 3, "g.axon");
        // Note: gutter=1, so empty_gutter=" " (one space). The
        // " --> ..." line therefore starts with two spaces ("<empty>"
        // + literal " --> ...").
        let expected = concat!(
            "  --> g.axon:2:3\n",
            "  |\n",
            "1 | alpha\n",
            "2 | beta\n",
            "  |   ^\n",
            "3 | gamma",
        );
        assert_eq!(out, expected);
    }

    #[test]
    fn golden_first_line_caret() {
        let src = "abc\ndef\n";
        let out = snippet(src, 1, 1, "x");
        let expected = concat!(
            "  --> x:1:1\n",
            "  |\n",
            "1 | abc\n",
            "  | ^\n",
            "2 | def",
        );
        assert_eq!(out, expected);
    }

    #[test]
    fn golden_two_digit_gutter() {
        let src: String = (1..=11)
            .map(|i| format!("L{i}"))
            .collect::<Vec<_>>()
            .join("\n");
        let out = snippet(&src, 10, 2, "big");
        let expected = concat!(
            "   --> big:10:2\n",
            "   |\n",
            " 8 | L8\n",
            " 9 | L9\n",
            "10 | L10\n",
            "   |  ^\n",
            "11 | L11",
        );
        assert_eq!(out, expected);
    }
}

// ── §Fase 28.e — Parser integration tests for smart-suggest ──────────────────
//
// Mirror of `tests/test_fase28_smart_suggest.py::TestParserIntegration`.
// Verifies that the parser actually wires `suggest_for` into the
// unknown-keyword diagnostic at both error sites — top-level and
// flow-body.
#[cfg(test)]
mod fase28_smart_suggest_parser_tests {
    use super::*;
    use crate::lexer::Lexer;

    fn lex(src: &str) -> Vec<Token> {
        Lexer::new(src, "<test>").tokenize().expect("lex")
    }

    #[test]
    fn top_level_typo_suggests_flow() {
        let src = "flwo F() { }";
        let err = Parser::new(lex(src)).parse().expect_err("must error");
        assert!(
            err.message.contains("Did you mean `flow`?"),
            "msg: {}",
            err.message
        );
    }

    #[test]
    fn top_level_unknown_far_no_suggestion() {
        let src = "qwerty F() { }";
        let err = Parser::new(lex(src)).parse().expect_err("must error");
        assert!(
            !err.message.contains("Did you mean"),
            "msg: {}",
            err.message
        );
    }

    #[test]
    fn flow_body_typo_suggests_step() {
        let src = "flow F() { stepp S {} }";
        let err = Parser::new(lex(src)).parse().expect_err("must error");
        assert!(
            err.message.contains("Did you mean `step`"),
            "msg: {}",
            err.message
        );
    }

    #[test]
    fn flow_body_typo_suggests_reason() {
        let src = "flow F() { reasn R {} }";
        let err = Parser::new(lex(src)).parse().expect_err("must error");
        assert!(
            err.message.contains("Did you mean `reason`?"),
            "msg: {}",
            err.message
        );
    }

    #[test]
    fn recovery_mode_carries_hint() {
        let src = "flwo F() { }";
        let result = Parser::new(lex(src)).parse_with_recovery();
        assert!(
            result
                .errors
                .iter()
                .any(|e| e.message.contains("Did you mean `flow`?")),
            "errors: {:?}",
            result.errors
        );
    }
}

// ── §Fase 35.m — mutate / purge where-clause capture ────────────────

#[cfg(test)]
mod fase35m_mutate_purge_where_tests {
    use super::*;

    fn parse(src: &str) -> Program {
        let tokens = crate::lexer::Lexer::new(src, "<test>")
            .tokenize()
            .expect("lex");
        Parser::new(tokens).parse().expect("parse")
    }

    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
        for d in &prog.declarations {
            if let Declaration::Flow(f) = d {
                if f.name == flow {
                    return f.body.first().expect("flow has at least one step");
                }
            }
        }
        panic!("flow `{flow}` not found");
    }

    #[test]
    fn mutate_captures_its_where_clause() {
        // Pre-35.m the `{ where: }` block was skipped — every mutate
        // ran whole-store. It must now reach `where_expr`.
        let prog =
            parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
        match first_step(&prog, "F") {
            FlowStep::Mutate(m) => {
                assert_eq!(m.store_name, "accounts");
                assert_eq!(m.where_expr, "id = 1");
            }
            other => panic!("expected Mutate, got {other:?}"),
        }
    }

    #[test]
    fn purge_captures_its_where_clause() {
        let prog =
            parse("flow F() -> Unit { purge logs { where: \"ts < 100\" } }");
        match first_step(&prog, "F") {
            FlowStep::Purge(p) => {
                assert_eq!(p.store_name, "logs");
                assert_eq!(p.where_expr, "ts < 100");
            }
            other => panic!("expected Purge, got {other:?}"),
        }
    }

    #[test]
    fn mutate_without_a_where_block_is_a_whole_store_op() {
        // No `{ where: }` → an empty filter → the runtime renders
        // `WHERE TRUE` (every row). A valid, intentional op.
        let prog = parse("flow F() -> Unit { mutate accounts }");
        match first_step(&prog, "F") {
            FlowStep::Mutate(m) => {
                assert_eq!(m.store_name, "accounts");
                assert_eq!(m.where_expr, "");
            }
            other => panic!("expected Mutate, got {other:?}"),
        }
    }
}

// ── §Fase 35.o — persist field-block capture ────────────────────────

#[cfg(test)]
mod fase35o_persist_fields_tests {
    use super::*;

    fn parse(src: &str) -> Program {
        let tokens = crate::lexer::Lexer::new(src, "<test>")
            .tokenize()
            .expect("lex");
        Parser::new(tokens).parse().expect("parse")
    }

    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
        for d in &prog.declarations {
            if let Declaration::Flow(f) = d {
                if f.name == flow {
                    return f.body.first().expect("flow has at least one step");
                }
            }
        }
        panic!("flow `{flow}` not found");
    }

    #[test]
    fn persist_captures_its_field_block() {
        // Pre-35.o the `{ col: value }` block was skipped — every
        // persist wrote the whole binding context. It must now reach
        // `fields`, in source order, with value expressions raw.
        let prog = parse(
            "flow F() -> Unit { persist into chat_history { \
             session_id: \"${session_id}\" sender: \"user\" \
             content: \"${message}\" } }",
        );
        match first_step(&prog, "F") {
            FlowStep::Persist(p) => {
                assert_eq!(p.store_name, "chat_history");
                assert_eq!(
                    p.fields,
                    vec![
                        ("session_id".to_string(), "${session_id}".to_string()),
                        ("sender".to_string(), "user".to_string()),
                        ("content".to_string(), "${message}".to_string()),
                    ]
                );
            }
            other => panic!("expected Persist, got {other:?}"),
        }
    }

    #[test]
    fn persist_without_a_block_keeps_the_user_bindings_fallback() {
        // No `{ }` → empty `fields` → the runtime falls back to the
        // v1.30.0 user-bindings row. Backward-compatible.
        let prog = parse("flow F() -> Unit { persist events }");
        match first_step(&prog, "F") {
            FlowStep::Persist(p) => {
                assert_eq!(p.store_name, "events");
                assert!(p.fields.is_empty());
            }
            other => panic!("expected Persist, got {other:?}"),
        }
    }

    #[test]
    fn persist_accepts_the_optional_into_connector() {
        // `persist into X` and `persist X` resolve to the SAME store
        // name — pre-35.o `into` was captured AS the store name.
        let with =
            parse("flow F() -> Unit { persist into accounts { id: \"1\" } }");
        let without =
            parse("flow F() -> Unit { persist accounts { id: \"1\" } }");
        for prog in [&with, &without] {
            match first_step(prog, "F") {
                FlowStep::Persist(p) => assert_eq!(p.store_name, "accounts"),
                other => panic!("expected Persist, got {other:?}"),
            }
        }
    }

    #[test]
    fn persist_into_without_a_block_resolves_the_store_name() {
        // `persist into events` — the `into` connector is skipped, the
        // store name is `events` (not `into`). Lateral bug closed.
        let prog = parse("flow F() -> Unit { persist into events }");
        match first_step(&prog, "F") {
            FlowStep::Persist(p) => {
                assert_eq!(p.store_name, "events");
                assert!(p.fields.is_empty());
            }
            other => panic!("expected Persist, got {other:?}"),
        }
    }

    #[test]
    fn persist_fields_lower_into_the_ir() {
        // The IR generator must carry `fields` onto `IRPersistStep`
        // so the runtime reads exactly the declared columns.
        let prog = parse(
            "flow F() -> Unit { persist into chat { content: \"${msg}\" } }",
        );
        let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
        let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
        match flow.steps.first().expect("one step") {
            crate::ir_nodes::IRFlowNode::Persist(p) => {
                assert_eq!(p.store_name, "chat");
                assert_eq!(
                    p.fields,
                    vec![("content".to_string(), "${msg}".to_string())]
                );
            }
            other => panic!("expected IRFlowNode::Persist, got {other:?}"),
        }
    }
}

// ── §Fase 35.p — mutate SET-field-block capture ─────────────────────

#[cfg(test)]
mod fase35p_mutate_fields_tests {
    use super::*;

    fn parse(src: &str) -> Program {
        let tokens = crate::lexer::Lexer::new(src, "<test>")
            .tokenize()
            .expect("lex");
        Parser::new(tokens).parse().expect("parse")
    }

    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
        for d in &prog.declarations {
            if let Declaration::Flow(f) = d {
                if f.name == flow {
                    return f.body.first().expect("flow has at least one step");
                }
            }
        }
        panic!("flow `{flow}` not found");
    }

    #[test]
    fn mutate_captures_its_set_field_block() {
        // Pre-35.p every key but `where:` was skipped — the runtime
        // SET every flow binding. The SET columns must now reach
        // `fields`, in source order, with `where:` still captured.
        let prog = parse(
            "flow F() -> Unit { mutate accounts { where: \"id = ${id}\" \
             balance: \"${new_balance}\" status: \"active\" } }",
        );
        match first_step(&prog, "F") {
            FlowStep::Mutate(m) => {
                assert_eq!(m.store_name, "accounts");
                assert_eq!(m.where_expr, "id = ${id}");
                assert_eq!(
                    m.fields,
                    vec![
                        ("balance".to_string(), "${new_balance}".to_string()),
                        ("status".to_string(), "active".to_string()),
                    ]
                );
            }
            other => panic!("expected Mutate, got {other:?}"),
        }
    }

    #[test]
    fn mutate_where_only_block_has_no_set_fields() {
        // A `{ where: }`-only block → empty `fields` → the runtime
        // falls back to the v1.31.0 user-bindings SET.
        let prog =
            parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
        match first_step(&prog, "F") {
            FlowStep::Mutate(m) => {
                assert_eq!(m.where_expr, "id = 1");
                assert!(m.fields.is_empty());
            }
            other => panic!("expected Mutate, got {other:?}"),
        }
    }

    #[test]
    fn mutate_with_no_block_is_a_whole_store_op() {
        // No block at all → empty where + empty fields (a whole-store
        // UPDATE from user bindings) — unchanged from 35.m.
        let prog = parse("flow F() -> Unit { mutate accounts }");
        match first_step(&prog, "F") {
            FlowStep::Mutate(m) => {
                assert_eq!(m.store_name, "accounts");
                assert_eq!(m.where_expr, "");
                assert!(m.fields.is_empty());
            }
            other => panic!("expected Mutate, got {other:?}"),
        }
    }

    #[test]
    fn mutate_fields_lower_into_the_ir() {
        let prog = parse(
            "flow F() -> Unit { mutate t { where: \"id = 1\" v: \"${x}\" } }",
        );
        let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
        let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
        match flow.steps.first().expect("one step") {
            crate::ir_nodes::IRFlowNode::Mutate(m) => {
                assert_eq!(m.where_expr, "id = 1");
                assert_eq!(
                    m.fields,
                    vec![("v".to_string(), "${x}".to_string())]
                );
            }
            other => panic!("expected IRFlowNode::Mutate, got {other:?}"),
        }
    }
}

