Skip to main content

axon_frontend/
parser.rs

1//! AXON Parser — recursive descent, fail-fast.
2//!
3//! Direct port of axon/compiler/parser.py.
4//!
5//! Tier 1 constructs (persona, context, anchor, memory, tool, type,
6//! flow, step, intent, run, epistemic, if, for, let, return) are
7//! fully parsed into typed AST nodes.
8//!
9//! Tier 2+ constructs are parsed structurally (balanced braces) into
10//! `GenericDeclaration` / `GenericFlowStep`.
11
12use crate::ast::*;
13use crate::tokens::{is_declaration_keyword, Token, TokenType, Trivia, TriviaKind};
14
15// Comment token kinds the lexer now emits (Fase 14.a). The parser
16// filters these out of its working stream — they are materialised into
17// a parallel `Trivia` array indexed by effective-token position, then
18// attached to `Program.declaration_trivia[i]` once each declaration's
19// span is known.
20const fn is_comment_token(tt: &TokenType) -> bool {
21    matches!(
22        tt,
23        TokenType::LineComment
24            | TokenType::BlockComment
25            | TokenType::DocLineComment
26            | TokenType::DocBlockComment
27            | TokenType::InnerDocLineComment
28            | TokenType::InnerDocBlockComment
29    )
30}
31
32const fn token_to_trivia_kind(tt: &TokenType) -> Option<TriviaKind> {
33    match tt {
34        TokenType::LineComment => Some(TriviaKind::Line),
35        TokenType::BlockComment => Some(TriviaKind::Block),
36        TokenType::DocLineComment => Some(TriviaKind::DocLine),
37        TokenType::DocBlockComment => Some(TriviaKind::DocBlock),
38        TokenType::InnerDocLineComment => Some(TriviaKind::InnerDocLine),
39        TokenType::InnerDocBlockComment => Some(TriviaKind::InnerDocBlock),
40        _ => None,
41    }
42}
43
44/// Fase 14.b — write `leading_trivia` and `trailing_trivia` into the
45/// per-struct fields of a `Declaration` variant.
46///
47/// Mirrors what the Python parser does automatically via its
48/// `_with_trivia` decorator on every `_parse_*` method. In Rust we
49/// do it once at the top of the parse loop so the spread to every
50/// variant is in a single place.
51fn attach_trivia_to_decl(decl: &mut Declaration, leading: Vec<Trivia>, trailing: Vec<Trivia>) {
52    match decl {
53        Declaration::Import(n) => {
54            n.leading_trivia = leading;
55            n.trailing_trivia = trailing;
56        }
57        Declaration::Persona(n) => {
58            n.leading_trivia = leading;
59            n.trailing_trivia = trailing;
60        }
61        Declaration::Context(n) => {
62            n.leading_trivia = leading;
63            n.trailing_trivia = trailing;
64        }
65        Declaration::Anchor(n) => {
66            n.leading_trivia = leading;
67            n.trailing_trivia = trailing;
68        }
69        Declaration::Memory(n) => {
70            n.leading_trivia = leading;
71            n.trailing_trivia = trailing;
72        }
73        Declaration::Tool(n) => {
74            n.leading_trivia = leading;
75            n.trailing_trivia = trailing;
76        }
77        Declaration::Type(n) => {
78            n.leading_trivia = leading;
79            n.trailing_trivia = trailing;
80        }
81        Declaration::Flow(n) => {
82            n.leading_trivia = leading;
83            n.trailing_trivia = trailing;
84        }
85        Declaration::Intent(n) => {
86            n.leading_trivia = leading;
87            n.trailing_trivia = trailing;
88        }
89        Declaration::Run(n) => {
90            n.leading_trivia = leading;
91            n.trailing_trivia = trailing;
92        }
93        Declaration::Epistemic(n) => {
94            n.leading_trivia = leading;
95            n.trailing_trivia = trailing;
96        }
97        Declaration::Let(n) => {
98            n.leading_trivia = leading;
99            n.trailing_trivia = trailing;
100        }
101        Declaration::LambdaData(n) => {
102            n.leading_trivia = leading;
103            n.trailing_trivia = trailing;
104        }
105        Declaration::Agent(n) => {
106            n.leading_trivia = leading;
107            n.trailing_trivia = trailing;
108        }
109        Declaration::Shield(n) => {
110            n.leading_trivia = leading;
111            n.trailing_trivia = trailing;
112        }
113        Declaration::Pix(n) => {
114            n.leading_trivia = leading;
115            n.trailing_trivia = trailing;
116        }
117        Declaration::Ledger(n) => {
118            n.leading_trivia = leading;
119            n.trailing_trivia = trailing;
120        }
121        Declaration::Psyche(n) => {
122            n.leading_trivia = leading;
123            n.trailing_trivia = trailing;
124        }
125        Declaration::Corpus(n) => {
126            n.leading_trivia = leading;
127            n.trailing_trivia = trailing;
128        }
129        Declaration::Dataspace(n) => {
130            n.leading_trivia = leading;
131            n.trailing_trivia = trailing;
132        }
133        Declaration::Ots(n) => {
134            n.leading_trivia = leading;
135            n.trailing_trivia = trailing;
136        }
137        Declaration::Mandate(n) => {
138            n.leading_trivia = leading;
139            n.trailing_trivia = trailing;
140        }
141        Declaration::Compute(n) => {
142            n.leading_trivia = leading;
143            n.trailing_trivia = trailing;
144        }
145        Declaration::Daemon(n) => {
146            n.leading_trivia = leading;
147            n.trailing_trivia = trailing;
148        }
149        Declaration::Extension(n) => {
150            n.leading_trivia = leading;
151            n.trailing_trivia = trailing;
152        }
153        Declaration::AxonStore(n) => {
154            n.leading_trivia = leading;
155            n.trailing_trivia = trailing;
156        }
157        Declaration::AxonEndpoint(n) => {
158            n.leading_trivia = leading;
159            n.trailing_trivia = trailing;
160        }
161        Declaration::Resource(n) => {
162            n.leading_trivia = leading;
163            n.trailing_trivia = trailing;
164        }
165        Declaration::Fabric(n) => {
166            n.leading_trivia = leading;
167            n.trailing_trivia = trailing;
168        }
169        Declaration::Manifest(n) => {
170            n.leading_trivia = leading;
171            n.trailing_trivia = trailing;
172        }
173        Declaration::Observe(n) => {
174            n.leading_trivia = leading;
175            n.trailing_trivia = trailing;
176        }
177        Declaration::Reconcile(n) => {
178            n.leading_trivia = leading;
179            n.trailing_trivia = trailing;
180        }
181        Declaration::Lease(n) => {
182            n.leading_trivia = leading;
183            n.trailing_trivia = trailing;
184        }
185        Declaration::Ensemble(n) => {
186            n.leading_trivia = leading;
187            n.trailing_trivia = trailing;
188        }
189        Declaration::Session(n) => {
190            n.leading_trivia = leading;
191            n.trailing_trivia = trailing;
192        }
193        Declaration::Topology(n) => {
194            n.leading_trivia = leading;
195            n.trailing_trivia = trailing;
196        }
197        Declaration::Immune(n) => {
198            n.leading_trivia = leading;
199            n.trailing_trivia = trailing;
200        }
201        Declaration::Reflex(n) => {
202            n.leading_trivia = leading;
203            n.trailing_trivia = trailing;
204        }
205        Declaration::Heal(n) => {
206            n.leading_trivia = leading;
207            n.trailing_trivia = trailing;
208        }
209        Declaration::Component(n) => {
210            n.leading_trivia = leading;
211            n.trailing_trivia = trailing;
212        }
213        Declaration::View(n) => {
214            n.leading_trivia = leading;
215            n.trailing_trivia = trailing;
216        }
217        Declaration::Channel(n) => {
218            n.leading_trivia = leading;
219            n.trailing_trivia = trailing;
220        }
221        Declaration::Socket(n) => {
222            n.leading_trivia = leading;
223            n.trailing_trivia = trailing;
224        }
225        Declaration::Observable(n) => {
226            n.leading_trivia = leading;
227            n.trailing_trivia = trailing;
228        }
229        Declaration::Witness(n) => {
230            n.leading_trivia = leading;
231            n.trailing_trivia = trailing;
232        }
233        Declaration::Generic(n) => {
234            n.leading_trivia = leading;
235            n.trailing_trivia = trailing;
236        }
237    }
238}
239
240// ── Public error type ────────────────────────────────────────────────────────
241
242/// §Fase 28.d — Source-context constants. D4 ratified 2026-05-10:
243/// 2 lines before + 2 lines after the error line. Mirror of the
244/// Python-side `_SOURCE_CONTEXT_LINES_BEFORE` / `_AFTER` so the
245/// rustc-style block has identical shape across stacks.
246pub const SOURCE_CONTEXT_LINES_BEFORE: usize = 2;
247pub const SOURCE_CONTEXT_LINES_AFTER: usize = 2;
248
249/// §Fase 28.d — Rustc-style source-context block for a parse error.
250///
251/// Holds a reference to the source text plus the line/column the
252/// error points at. Rendering is lazy — call ``render()`` to format
253/// the block (line numbers + caret + 2 lines before + 2 after).
254///
255/// Pure and deterministic: no ANSI colors, no terminal-width
256/// detection. Output shape is byte-identical to the Python
257/// `SourceSnippet.render()` on the same input — that's the cross-
258/// stack drift gate (28.i).
259#[derive(Debug, Clone)]
260pub struct SourceSnippet {
261    pub source: String,
262    pub line: u32,
263    pub column: u32,
264    pub filename: String,
265    pub context_before: usize,
266    pub context_after: usize,
267}
268
269impl SourceSnippet {
270    /// Construct with the default 2/2 context window.
271    pub fn new(source: String, line: u32, column: u32, filename: String) -> Self {
272        Self {
273            source,
274            line,
275            column,
276            filename,
277            context_before: SOURCE_CONTEXT_LINES_BEFORE,
278            context_after: SOURCE_CONTEXT_LINES_AFTER,
279        }
280    }
281
282    /// Format the snippet as a multi-line rustc-style block.
283    ///
284    /// Empty source → empty string. Out-of-range line → empty
285    /// string. Caret column is clamped to `[1, line_len + 1]`.
286    /// Output shape matches Python `SourceSnippet.render` byte-
287    /// identically per D7.
288    #[must_use]
289    pub fn render(&self) -> String {
290        if self.source.is_empty() || self.line < 1 {
291            return String::new();
292        }
293        let raw: Vec<&str> = self.source.split('\n').collect();
294        // Match Python's str.splitlines() trailing-newline shape:
295        // strip an empty trailing entry produced by a final '\n'.
296        let lines: Vec<&str> = if raw.last() == Some(&"") {
297            raw[..raw.len() - 1].to_vec()
298        } else {
299            raw
300        };
301        if lines.is_empty() || self.line as usize > lines.len() {
302            return String::new();
303        }
304
305        let line_idx = self.line as usize;
306        let start = line_idx.saturating_sub(self.context_before).max(1);
307        let end = (line_idx + self.context_after).min(lines.len());
308
309        let gutter = end.to_string().len();
310        let empty_gutter = " ".repeat(gutter);
311
312        let mut out: Vec<String> = Vec::with_capacity(end - start + 4);
313        out.push(format!(
314            "{empty_gutter} --> {}:{}:{}",
315            self.filename, self.line, self.column
316        ));
317        out.push(format!("{empty_gutter} |"));
318        for n in start..=end {
319            let line_text = lines[n - 1];
320            out.push(format!("{n:>gutter$} | {line_text}", gutter = gutter));
321            if n == line_idx {
322                let line_len = line_text.chars().count();
323                let col = (self.column as usize).clamp(1, line_len + 1);
324                out.push(format!(
325                    "{empty_gutter} | {pad}^",
326                    pad = " ".repeat(col - 1)
327                ));
328            }
329        }
330        out.join("\n")
331    }
332}
333
334#[derive(Debug, Clone, Default)]
335pub struct ParseError {
336    pub message: String,
337    pub line: u32,
338    pub column: u32,
339    /// §Fase 28.d — Optional rustc-style source-context block.
340    /// `None` preserves the legacy single-line shape; populated by
341    /// `Parser::with_source` callers (and by `parse_with_recovery`
342    /// / `parse` when a source has been attached to the parser).
343    /// Existing struct-literal call sites use the `..Default::default()`
344    /// idiom (default = None) to stay terse.
345    pub source_snippet: Option<SourceSnippet>,
346}
347
348impl ParseError {
349    /// §Fase 28.d — Attach a `SourceSnippet` derived from raw source
350    /// text and filename. Returns `self` so the call can be chained
351    /// at the construction site. No-op when `line == 0`. Idempotent.
352    #[must_use]
353    pub fn attach_source(mut self, source: &str, filename: &str) -> Self {
354        if self.line >= 1 {
355            self.source_snippet = Some(SourceSnippet::new(
356                source.to_string(),
357                self.line,
358                self.column,
359                filename.to_string(),
360            ));
361        }
362        self
363    }
364}
365
366impl std::fmt::Display for ParseError {
367    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368        write!(f, "[line {}:{}] {}", self.line, self.column, self.message)?;
369        if let Some(snippet) = &self.source_snippet {
370            let block = snippet.render();
371            if !block.is_empty() {
372                write!(f, "\n{block}")?;
373            }
374        }
375        Ok(())
376    }
377}
378
379impl std::error::Error for ParseError {}
380
381// ── §Fase 28.c — Public recovery result ──────────────────────────────────────
382//
383// Mirror of Python's `axon.compiler.parser.ParseResult` (Fase 28.b).
384// The rationale, sync semantics, and test contract are documented in
385// `docs/fase/fase_28_adopter_diagnostic_robustness.md`. The Rust frontend
386// must produce structurally identical error lists to the Python parser
387// when handed the same source — that is the cross-stack drift gate
388// (D7 ratified 2026-05-10: byte-identical error lists).
389//
390// `program` holds whatever declarations the parser was able to parse
391// successfully. `errors` holds every recovered error in source order.
392// A clean parse returns `errors.is_empty()`; the existing fail-fast
393// `parse()` API is preserved verbatim per D9.
394
395/// Outcome of `Parser::parse_with_recovery` — partial program plus the
396/// list of every error the parser recovered from. See module docs for
397/// the panic-mode + sync-point recovery semantics.
398#[derive(Debug)]
399pub struct ParseResult {
400    pub program: Program,
401    pub errors: Vec<ParseError>,
402}
403
404impl ParseResult {
405    /// True iff at least one parse error was recovered. Callers that
406    /// want to short-circuit on failure should check this rather than
407    /// relying on `program.declarations.is_empty()` (the parser may
408    /// have salvaged some declarations even with errors present).
409    #[inline]
410    #[must_use]
411    pub fn has_errors(&self) -> bool {
412        !self.errors.is_empty()
413    }
414
415    /// Inverse of `has_errors`. Convenience for the "happy path" check
416    /// in tests + adopter integrations.
417    #[inline]
418    #[must_use]
419    pub fn is_clean(&self) -> bool {
420        self.errors.is_empty()
421    }
422}
423
424/// §Fase 28.c — Top-level declaration keywords used as resync points
425/// during error recovery (D2 ratified 2026-05-10). Mirrors the
426/// `_TOP_LEVEL_DECLARATION_KEYWORDS` frozenset on the Python side.
427///
428/// Distinct from `tokens::is_declaration_keyword` because that helper
429/// is used by the structural declaration counter and intentionally
430/// excludes some grammar-only tokens (Know/Believe/Speculate/Doubt,
431/// Ingest, Ots) that DO begin a top-level declaration in
432/// `parse_declaration` and therefore must be valid sync points.
433///
434/// Adding a new top-level dispatch arm in `parse_declaration` MUST
435/// add the corresponding token here so the recovery walker can
436/// re-sync correctly.
437#[inline]
438const fn is_top_level_decl_kw_for_recovery(tt: &TokenType) -> bool {
439    matches!(
440        tt,
441        TokenType::Import
442            | TokenType::Persona
443            | TokenType::Context
444            | TokenType::Anchor
445            | TokenType::Memory
446            | TokenType::Tool
447            | TokenType::Type
448            | TokenType::Flow
449            | TokenType::Intent
450            | TokenType::Run
451            | TokenType::Let
452            | TokenType::Know
453            | TokenType::Believe
454            | TokenType::Speculate
455            | TokenType::Doubt
456            | TokenType::Lambda
457            | TokenType::Agent
458            | TokenType::Shield
459            | TokenType::Pix
460            | TokenType::Ledger
461            | TokenType::Psyche
462            | TokenType::Corpus
463            | TokenType::Dataspace
464            | TokenType::Ots
465            | TokenType::Mandate
466            | TokenType::Compute
467            | TokenType::Daemon
468            | TokenType::AxonStore
469            | TokenType::AxonEndpoint
470            | TokenType::Resource
471            | TokenType::Fabric
472            | TokenType::Manifest
473            | TokenType::Observe
474            | TokenType::Reconcile
475            | TokenType::Lease
476            | TokenType::Ensemble
477            | TokenType::Session
478            | TokenType::Topology
479            | TokenType::Immune
480            | TokenType::Reflex
481            | TokenType::Heal
482            | TokenType::Component
483            | TokenType::View
484            | TokenType::Channel
485            | TokenType::Ingest
486            | TokenType::Persist
487            | TokenType::Retrieve
488            | TokenType::Mutate
489            | TokenType::Purge
490            | TokenType::Transact
491            | TokenType::Mcp
492    )
493}
494
495// ── §Fase 30.b — axonendpoint transport + keepalive closed enums ────────────
496//
497// D2 ratified 2026-05-10: `transport` is a closed enum
498// {json, sse, ndjson}. D6 ratified: `keepalive` is a closed enum
499// {5s, 15s, 30s, 60s}. Both mirror the Python frontend's
500// `_AXONENDPOINT_TRANSPORT_VALUES` / `_AXONENDPOINT_KEEPALIVE_VALUES`
501// frozensets in `axon/compiler/parser.py`. Cross-stack drift gate
502// (30.b fixture) asserts byte-identical parse for every entry.
503
504/// Adopter-facing acceptable values for `transport:` field.
505/// Used by both the parser (validation + smart-suggest) and the
506/// type-checker (30.c) so adopter tooling sees one canonical list.
507pub const AXONENDPOINT_TRANSPORT_VALUES: &[&str] = &["json", "sse", "ndjson"];
508
509/// §Fase 33.z.k.b (v1.28.0) — Closed-catalog SSE wire-format
510/// dialects. Selected via the parametrized grammar
511/// `transport: sse(<dialect>)`; bare `transport: sse` resolves to
512/// the Q1 default per the flow's algebraic-effect predicate
513/// (openai for tool-streaming flows; axon for type-annotation-only).
514///
515/// Vertical-grounded scope (Q3 revised 2026-05-14): five dialects
516/// cover ~99% of LLM-streaming adopter expectations.
517///   - `axon`      — current W3C named events
518///                   (event: axon.token / event: axon.complete).
519///                   D6 backwards-compat baseline; indefinitely
520///                   supported as a first-class option.
521///   - `openai`    — `data: {"choices":[{"delta":{...}}]}` frames
522///                   terminated by `data: [DONE]`. OpenAI Chat
523///                   Completions streaming wire verbatim.
524///   - `kimi`      — Moonshot Kimi (kimi.moonshot.cn) — uses the
525///                   OpenAI-compatible Chat Completions wire format
526///                   verbatim (same chunk shape, same `data: [DONE]`
527///                   sentinel). First-class entry so adopters
528///                   declare intent explicitly; under the hood the
529///                   wire is identical to `openai`.
530///   - `glm`       — Zhipu ChatGLM (open.bigmodel.cn) — same as
531///                   kimi, uses OpenAI-compat wire. First-class
532///                   entry for adopter clarity.
533///   - `anthropic` — `event: content_block_delta` frames terminated
534///                   by `event: message_stop`. Adopter SDKs
535///                   targeting Anthropic Claude consume this shape
536///                   verbatim.
537///
538/// Why kimi + glm as first-class entries (Q3 revision rationale):
539/// Bemarking AI's primary adopter pipelines through Kimi K2.x +
540/// Zhipu GLM-4.x. While the wire IS byte-identical to OpenAI's
541/// Chat Completions streaming, declaring `transport: sse(kimi)` /
542/// `transport: sse(glm)` lets the audit trail + observability
543/// surfaces correlate adopter intent against the underlying
544/// provider — without the adopter having to know that "kimi
545/// happens to be OpenAI-compat on the wire today". The runtime
546/// dispatches kimi + glm to the same `OpenAIDialectAdapter` so
547/// the wire shape stays canonical-OpenAI-bytes.
548///
549/// Open-set adapter pluggability (downstream crates registering
550/// custom dialects) remains explicitly out of scope per the
551/// Axon-for-Axon discipline.
552pub const AXONENDPOINT_TRANSPORT_DIALECTS: &[&str] =
553    &["axon", "openai", "kimi", "glm", "anthropic"];
554
555/// Adopter-facing acceptable values for `keepalive:` field.
556pub const AXONENDPOINT_KEEPALIVE_VALUES: &[&str] = &["5s", "15s", "30s", "60s"];
557
558/// §Fase 32.b D3 — Closed method enum for `method:` field. Adopter-
559/// declarable methods only; HEAD/OPTIONS/CONNECT/TRACE are
560/// runtime-managed (CORS preflight, etc.) and never declared from
561/// source. Closed enum refuses interpretation drift; smart-suggest
562/// catches near-misses at parse time.
563pub const AXONENDPOINT_METHOD_VALUES: &[&str] = &["GET", "POST", "PUT", "DELETE", "PATCH"];
564
565/// §Fase 36.d (D2) — Closed catalog for the `axonendpoint backend:`
566/// declaration. The set is `CANONICAL_PROVIDERS ∪ {auto, stub}`:
567///
568///   - the seven canonical LLM providers — `anthropic`, `gemini`,
569///     `glm`, `kimi`, `ollama`, `openai`, `openrouter` — a concrete,
570///     declared backend that rung 2 of the Fase 36 D1 resolution
571///     ladder fires immediately;
572///   - `auto` — transparent: declaring it is equivalent to omitting
573///     `backend:` entirely (the route resolves down the ladder —
574///     server default → environment-available providers);
575///   - `stub` — the no-op backend, reachable ONLY by an explicit,
576///     written declaration (D5: a silent degradation to `stub` is
577///     forbidden; an explicit opt-in is not).
578///
579/// `axon-frontend` carries zero runtime deps and therefore cannot
580/// import `axon::backends::CANONICAL_PROVIDERS`; this list is a
581/// hand-maintained mirror. The axon-rs drift gate
582/// (`tests/fase36_d_backend_catalog_drift.rs`) asserts the two stay
583/// byte-identical — adding a provider in one place without the other
584/// fails CI.
585pub const AXONENDPOINT_BACKEND_VALUES: &[&str] = &[
586    "anthropic",
587    "auto",
588    "gemini",
589    "glm",
590    "kimi",
591    "ollama",
592    "openai",
593    "openrouter",
594    "stub",
595];
596
597#[inline]
598fn axonendpoint_is_valid_transport(s: &str) -> bool {
599    AXONENDPOINT_TRANSPORT_VALUES.iter().any(|&v| v == s)
600}
601
602#[inline]
603fn axonendpoint_is_valid_method(s: &str) -> bool {
604    AXONENDPOINT_METHOD_VALUES.iter().any(|&v| v == s)
605}
606
607#[inline]
608fn axonendpoint_is_valid_backend(s: &str) -> bool {
609    AXONENDPOINT_BACKEND_VALUES.iter().any(|&v| v == s)
610}
611
612#[inline]
613fn axonendpoint_is_valid_keepalive(s: &str) -> bool {
614    AXONENDPOINT_KEEPALIVE_VALUES.iter().any(|&v| v == s)
615}
616
617/// §Fase 37.y (D2) — Closed type catalog for query parameters.
618///
619/// Query values arrive over HTTP as URL-encoded strings; the catalog
620/// is the set of types axon will validate / coerce them into for the
621/// Request Binding Contract. Hand-curated, intentionally small:
622///   - `Text` — the raw string (always succeeds)
623///   - `Int` — `i64` parseable
624///   - `Float` — `f64` parseable, finite
625///   - `Bool` — case-insensitive `{true, false, 1, 0, yes, no, on, off}`
626///   - `Uuid` — RFC 4122 textual form
627///
628/// Extending the catalog is a future axon-T?nn surface; v1.38.5 ships
629/// the 5 types covering ~95% of REST query patterns. Lists / dates /
630/// datetimes / enums are honest deferrals (see §7 of the plan vivo).
631pub const AXONENDPOINT_QUERY_PARAM_TYPES: &[&str] =
632    &["Text", "Int", "Float", "Bool", "Uuid"];
633
634/// `true` iff `s` is one of the §Fase 37.y (D2) query-param catalog
635/// entries — exact case-sensitive match (axon types are PascalCase).
636#[inline]
637pub(crate) fn axonendpoint_is_valid_query_param_type(s: &str) -> bool {
638    AXONENDPOINT_QUERY_PARAM_TYPES.iter().any(|&v| v == s)
639}
640
641/// §Fase 37.y (D1) — Extract `{name}` placeholder names from an
642/// `axonendpoint` `path:` string, in left-to-right declaration order.
643///
644/// Recognized placeholder grammar (single-segment, no nested braces):
645/// `{NAME}` where `NAME` matches `[A-Za-z_][A-Za-z0-9_]*`. Anything
646/// inside braces that does NOT match the identifier shape is silently
647/// IGNORED — it's either an adopter typo (caught later by axum at
648/// route registration) or a literal brace in the URL pattern.
649///
650/// Returns `Err(duplicate_name)` when the same `{name}` appears more
651/// than once in the path — HTTP route patterns reject duplicates
652/// structurally (`axum` would panic at registration), so surfacing
653/// the error at parse time is the right place.
654///
655/// Pure + total: never panics; deterministic over its single string
656/// argument. Hand-rolled scanner (no regex dep at parser layer).
657///
658/// # Examples
659///
660/// - `"/api/users"` → `Ok(vec![])`
661/// - `"/api/users/{id}"` → `Ok(vec!["id"])`
662/// - `"/api/tenants/{tenant_id}/secrets/{secret_name}"`
663///   → `Ok(vec!["tenant_id", "secret_name"])`
664/// - `"/api/users/{id}/posts/{id}"` → `Err("id")` (duplicate)
665/// - `"/api/{not valid}"` → `Ok(vec![])` (malformed brace content
666///   silently ignored; axum surfaces the error at registration)
667pub(crate) fn extract_path_param_names(path: &str) -> Result<Vec<String>, String> {
668    let mut out: Vec<String> = Vec::new();
669    let bytes = path.as_bytes();
670    let mut i = 0;
671    while i < bytes.len() {
672        if bytes[i] != b'{' {
673            i += 1;
674            continue;
675        }
676        // Find the matching close brace; if none, the open brace is
677        // a literal — leave it alone.
678        let start = i + 1;
679        let mut end = start;
680        while end < bytes.len() && bytes[end] != b'}' {
681            end += 1;
682        }
683        if end == bytes.len() {
684            // Unterminated — give up; downstream parser/runtime
685            // surface the malformed path elsewhere.
686            break;
687        }
688        let raw = &path[start..end];
689        // Validate identifier shape: [A-Za-z_][A-Za-z0-9_]*
690        let valid = !raw.is_empty()
691            && raw.bytes().enumerate().all(|(idx, b)| {
692                if idx == 0 {
693                    b.is_ascii_alphabetic() || b == b'_'
694                } else {
695                    b.is_ascii_alphanumeric() || b == b'_'
696                }
697            });
698        if valid {
699            let name = raw.to_string();
700            if out.iter().any(|existing| existing == &name) {
701                return Err(name);
702            }
703            out.push(name);
704        }
705        i = end + 1;
706    }
707    Ok(out)
708}
709
710/// §Fase 32.g (D8) — Closed capability-slug grammar. Validates a
711/// `requires:` slug per `^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$`.
712///
713/// Hand-rolled (no regex dep at parser layer) — each segment must
714/// match `[a-z][a-z0-9_]*` and segments are joined by single dots.
715/// Public so the runtime mirror (`axon::auth_scope`) reuses the same
716/// predicate without duplicating the rule.
717///
718/// Examples valid: `admin`, `legal.read`, `hipaa.phi.read`,
719/// `bank.officer.senior`, `a`, `a_b`, `a1`.
720/// Examples invalid: empty, `Admin` (uppercase), `1admin` (digit
721/// first), `bank-officer` (hyphen), `bank..a` (empty segment),
722/// `.admin`, `admin.`, `admin..` .
723pub fn is_valid_capability_slug(slug: &str) -> bool {
724    if slug.is_empty() {
725        return false;
726    }
727    for segment in slug.split('.') {
728        if !is_valid_slug_segment(segment) {
729            return false;
730        }
731    }
732    true
733}
734
735fn is_valid_slug_segment(seg: &str) -> bool {
736    let mut chars = seg.chars();
737    let first = match chars.next() {
738        Some(c) => c,
739        None => return false,
740    };
741    if !first.is_ascii_lowercase() {
742        return false;
743    }
744    chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
745}
746
747// ════════════════════════════════════════════════════════════════════
748//  §Fase 37.y (D1) — `extract_path_param_names` unit tests
749// ════════════════════════════════════════════════════════════════════
750
751// ════════════════════════════════════════════════════════════════════
752//  §Fase 37.y (D2) — `axonendpoint_is_valid_query_param_type` + the
753//  inline `query: { … }` parser, end-to-end through the lexer.
754// ════════════════════════════════════════════════════════════════════
755
756#[cfg(test)]
757mod query_param_catalog_tests {
758    use super::{axonendpoint_is_valid_query_param_type, AXONENDPOINT_QUERY_PARAM_TYPES};
759
760    #[test]
761    fn accepts_every_catalog_entry() {
762        for ty in AXONENDPOINT_QUERY_PARAM_TYPES {
763            assert!(
764                axonendpoint_is_valid_query_param_type(ty),
765                "catalog entry `{ty}` must validate"
766            );
767        }
768    }
769
770    #[test]
771    fn rejects_off_catalog_types() {
772        for off in &[
773            "Timestamp",    // not in v1.38.5 — list/dates deferred
774            "Date",
775            "DateTime",
776            "List<Text>",   // multi-value query params deferred (§7)
777            "Jsonb",        // store-only types not query-applicable
778            "Bytea",
779            "text",         // lowercase rejected (axon types are PascalCase)
780            "TEXT",
781            "Number",       // not in axon's type catalog at all
782            "",             // empty
783            " ",            // whitespace
784        ] {
785            assert!(
786                !axonendpoint_is_valid_query_param_type(off),
787                "off-catalog `{off}` must reject"
788            );
789        }
790    }
791
792    #[test]
793    fn catalog_size_matches_design() {
794        // The plan vivo D2 states a closed 5-type catalog. A future
795        // axon-T?nn surface may extend it; that requires updating BOTH
796        // the catalog AND the plan vivo §7 honest-scope note.
797        assert_eq!(AXONENDPOINT_QUERY_PARAM_TYPES.len(), 5);
798    }
799}
800
801#[cfg(test)]
802mod query_param_parser_tests {
803    use crate::lexer::Lexer;
804    use crate::parser::Parser;
805
806    fn parse_endpoint_source(src: &str) -> Result<crate::ast::AxonEndpointDefinition, String> {
807        let tokens = Lexer::new(src, "test.axon")
808            .tokenize()
809            .map_err(|e| format!("lex: {}", e.message))?;
810        let mut parser = Parser::new(tokens);
811        let program = parser.parse().map_err(|e| format!("parse: {}", e.message))?;
812        program
813            .declarations
814            .into_iter()
815            .find_map(|d| match d {
816                crate::ast::Declaration::AxonEndpoint(e) => Some(e),
817                _ => None,
818            })
819            .ok_or_else(|| "no axonendpoint in program".to_string())
820    }
821
822    #[test]
823    fn endpoint_with_no_query_block_keeps_empty_vec() {
824        let src = r#"
825            axonendpoint write_secret {
826                method: POST
827                path: "/api/users"
828                body: SecretWriteRequest
829                execute: WriteSecret
830            }
831        "#;
832        let ep = parse_endpoint_source(src).expect("parses");
833        assert!(
834            ep.query_params.is_empty(),
835            "D5 — no `query:` block ⇒ empty query_params"
836        );
837    }
838
839    #[test]
840    fn single_query_param_required() {
841        let src = r#"
842            axonendpoint list_users {
843                method: GET
844                path: "/api/users"
845                query: { status: Text }
846                execute: ListUsers
847            }
848        "#;
849        let ep = parse_endpoint_source(src).expect("parses");
850        assert_eq!(ep.query_params.len(), 1);
851        assert_eq!(ep.query_params[0].name, "status");
852        assert_eq!(ep.query_params[0].type_expr.name, "Text");
853        assert!(!ep.query_params[0].type_expr.optional);
854    }
855
856    #[test]
857    fn optional_query_param_via_question_suffix() {
858        let src = r#"
859            axonendpoint list_users {
860                method: GET
861                path: "/api/users"
862                query: { limit: Int? }
863                execute: ListUsers
864            }
865        "#;
866        let ep = parse_endpoint_source(src).expect("parses");
867        assert_eq!(ep.query_params.len(), 1);
868        assert_eq!(ep.query_params[0].name, "limit");
869        assert_eq!(ep.query_params[0].type_expr.name, "Int");
870        assert!(
871            ep.query_params[0].type_expr.optional,
872            "`?` suffix sets optional"
873        );
874    }
875
876    #[test]
877    fn multiple_query_params_preserve_declaration_order() {
878        let src = r#"
879            axonendpoint search {
880                method: GET
881                path: "/api/search"
882                query: { q: Text, page: Int?, limit: Int?, exact: Bool? }
883                execute: Search
884            }
885        "#;
886        let ep = parse_endpoint_source(src).expect("parses");
887        let names: Vec<&str> = ep.query_params.iter().map(|f| f.name.as_str()).collect();
888        assert_eq!(names, vec!["q", "page", "limit", "exact"]);
889        let types: Vec<&str> = ep
890            .query_params
891            .iter()
892            .map(|f| f.type_expr.name.as_str())
893            .collect();
894        assert_eq!(types, vec!["Text", "Int", "Int", "Bool"]);
895        let optionals: Vec<bool> = ep
896            .query_params
897            .iter()
898            .map(|f| f.type_expr.optional)
899            .collect();
900        assert_eq!(optionals, vec![false, true, true, true]);
901    }
902
903    #[test]
904    fn duplicate_query_param_is_parse_error() {
905        let src = r#"
906            axonendpoint bad {
907                method: GET
908                path: "/api/x"
909                query: { name: Text, name: Int? }
910                execute: Bad
911            }
912        "#;
913        let err = parse_endpoint_source(src).expect_err("must fail");
914        assert!(
915            err.contains("duplicate query param 'name'"),
916            "error must name the duplicate. Got: {err}"
917        );
918    }
919
920    #[test]
921    fn off_catalog_type_with_smart_suggest_hint() {
922        // `Strng` is one edit away from `Text` (would suggest `Text`?
923        // Actually edit distance to `Text` is 4; to `Int` is 5. Likely
924        // no smart suggestion within distance 2. The error still names
925        // the catalog explicitly.)
926        let src = r#"
927            axonendpoint bad {
928                method: GET
929                path: "/api/x"
930                query: { value: Strng }
931                execute: Bad
932            }
933        "#;
934        let err = parse_endpoint_source(src).expect_err("must fail");
935        assert!(
936            err.contains("unsupported type 'Strng'"),
937            "error must name the bad type. Got: {err}"
938        );
939        assert!(
940            err.contains("Expected one of: Text | Int | Float | Bool | Uuid"),
941            "error must list the closed catalog. Got: {err}"
942        );
943    }
944
945    #[test]
946    fn close_typo_gets_did_you_mean_hint() {
947        // `Txt` → edit distance 1 from `Text` → smart-suggest should
948        // surface the hint.
949        let src = r#"
950            axonendpoint bad {
951                method: GET
952                path: "/api/x"
953                query: { value: Txt }
954                execute: Bad
955            }
956        "#;
957        let err = parse_endpoint_source(src).expect_err("must fail");
958        assert!(
959            err.contains("Did you mean") && err.contains("`Text`"),
960            "smart-suggest must hint `Text`. Got: {err}"
961        );
962    }
963
964    #[test]
965    fn every_catalog_type_parses_cleanly() {
966        // Round-trip smoke for all 5 catalog entries.
967        for ty in &["Text", "Int", "Float", "Bool", "Uuid"] {
968            let src = format!(
969                r#"
970                    axonendpoint x {{
971                        method: GET
972                        path: "/api/x"
973                        query: {{ v: {ty} }}
974                        execute: X
975                    }}
976                "#
977            );
978            let ep = parse_endpoint_source(&src)
979                .unwrap_or_else(|e| panic!("`{ty}` should parse: {e}"));
980            assert_eq!(ep.query_params[0].type_expr.name, *ty);
981        }
982    }
983
984    #[test]
985    fn comma_optional_between_params() {
986        // The plan vivo design accepts both comma-separated and
987        // whitespace-separated query params (existing parser style is
988        // forgiving). Whitespace-only:
989        let src = r#"
990            axonendpoint x {
991                method: GET
992                path: "/api/x"
993                query: { a: Text b: Int? }
994                execute: X
995            }
996        "#;
997        let ep = parse_endpoint_source(src).expect("parses without commas");
998        assert_eq!(ep.query_params.len(), 2);
999    }
1000
1001    // ─── Robustness hardening (37.y.2 100% robust closure) ──────────
1002
1003    #[test]
1004    fn double_query_block_is_parse_error() {
1005        // An adopter who copy-pastes the `query:` block twice should
1006        // see a clear parse error, not a silent merge that produces
1007        // an unexpectedly-augmented endpoint with both blocks fused.
1008        let src = r#"
1009            axonendpoint x {
1010                method: GET
1011                path: "/api/x"
1012                query: { a: Text }
1013                query: { b: Int? }
1014                execute: X
1015            }
1016        "#;
1017        let err = parse_endpoint_source(src).expect_err("must fail");
1018        assert!(
1019            err.contains("declares `query: { … }` more than once"),
1020            "error must call out the duplicate block. Got: {err}"
1021        );
1022        assert!(
1023            err.contains("combine all params into a single block"),
1024            "error must hint the canonical fix. Got: {err}"
1025        );
1026    }
1027
1028    #[test]
1029    fn optional_generic_type_is_parse_error_with_canonical_hint() {
1030        // `Optional<Text>` is the wrong way to declare an optional
1031        // query param. The canonical syntax is `Text?` (the `?`
1032        // suffix). The error must surface this with a literal example.
1033        let src = r#"
1034            axonendpoint x {
1035                method: GET
1036                path: "/api/x"
1037                query: { value: Optional<Text> }
1038                execute: X
1039            }
1040        "#;
1041        let err = parse_endpoint_source(src).expect_err("must fail");
1042        assert!(
1043            err.contains("generic type `Optional<Text>`"),
1044            "error must name the generic type literally. Got: {err}"
1045        );
1046        assert!(
1047            err.contains("Use `Text?` (the `?` suffix)"),
1048            "error must hint the canonical `Text?` syntax. Got: {err}"
1049        );
1050    }
1051
1052    #[test]
1053    fn list_generic_type_is_parse_error_with_deferral_hint() {
1054        // Multi-value query params (`?tag=a&tag=b`) are honest-
1055        // deferred per the plan vivo §7. Adopters who write
1056        // `List<Text>` should see a clear error explaining the
1057        // deferral, not a confusing "type `List` not in catalog".
1058        let src = r#"
1059            axonendpoint x {
1060                method: GET
1061                path: "/api/x"
1062                query: { tags: List<Text> }
1063                execute: X
1064            }
1065        "#;
1066        let err = parse_endpoint_source(src).expect_err("must fail");
1067        assert!(
1068            err.contains("generic type `List<Text>`"),
1069            "error must name the generic type. Got: {err}"
1070        );
1071        assert!(
1072            err.contains("Multi-value query params")
1073                && err.contains("honest-deferred"),
1074            "error must mention the multi-value deferral. Got: {err}"
1075        );
1076    }
1077
1078    #[test]
1079    fn other_generic_types_caught_generically() {
1080        // Generic types beyond `Optional` and `List` get the
1081        // generic-rejection message without a canonical-syntax hint
1082        // (the catalog list is the canonical guidance).
1083        let src = r#"
1084            axonendpoint x {
1085                method: GET
1086                path: "/api/x"
1087                query: { value: Stream<Int> }
1088                execute: X
1089            }
1090        "#;
1091        let err = parse_endpoint_source(src).expect_err("must fail");
1092        assert!(
1093            err.contains("generic type `Stream<Int>`"),
1094            "error must name the generic type. Got: {err}"
1095        );
1096        assert!(
1097            err.contains("Text | Int | Float | Bool | Uuid"),
1098            "error must list the closed catalog. Got: {err}"
1099        );
1100    }
1101
1102    #[test]
1103    fn uuid_optional_parses_cleanly() {
1104        // Hardening companion — `Uuid?` is in the catalog AND
1105        // optional. The two features compose without surprise.
1106        let src = r#"
1107            axonendpoint find {
1108                method: GET
1109                path: "/api/x"
1110                query: { after: Uuid? }
1111                execute: Find
1112            }
1113        "#;
1114        let ep = parse_endpoint_source(src).expect("parses");
1115        assert_eq!(ep.query_params.len(), 1);
1116        assert_eq!(ep.query_params[0].name, "after");
1117        assert_eq!(ep.query_params[0].type_expr.name, "Uuid");
1118        assert!(ep.query_params[0].type_expr.optional);
1119        assert_eq!(ep.query_params[0].type_expr.generic_param, "");
1120    }
1121
1122    #[test]
1123    fn empty_query_block_yields_empty_vec() {
1124        // `query: { }` is grammatically valid but semantically a
1125        // no-op (equivalent to omitting the block). Don't error;
1126        // just record an empty Vec.
1127        let src = r#"
1128            axonendpoint x {
1129                method: GET
1130                path: "/api/x"
1131                query: { }
1132                execute: X
1133            }
1134        "#;
1135        let ep = parse_endpoint_source(src).expect("empty block parses");
1136        assert!(ep.query_params.is_empty());
1137    }
1138
1139    #[test]
1140    fn kivi_secret_write_path_plus_query() {
1141        // Combined path-param + query-param test: an endpoint that
1142        // takes IDs in the URL AND optional filters in the query
1143        // string. This is the natural REST shape Fase 37.y serves.
1144        let src = r#"
1145            axonendpoint write_secret {
1146                method: POST
1147                path: "/api/tenants/{tenant_id}/secrets/{secret_name}"
1148                query: { dry_run: Bool?, overwrite: Bool? }
1149                body: SecretWriteRequest
1150                execute: WriteSecret
1151            }
1152        "#;
1153        let ep = parse_endpoint_source(src).expect("parses");
1154        // Path params populated (from 37.y.1):
1155        assert_eq!(ep.path_params, vec!["tenant_id", "secret_name"]);
1156        // Query params populated (from this sub-fase 37.y.2):
1157        assert_eq!(ep.query_params.len(), 2);
1158        assert_eq!(ep.query_params[0].name, "dry_run");
1159        assert_eq!(ep.query_params[0].type_expr.name, "Bool");
1160        assert!(ep.query_params[0].type_expr.optional);
1161        assert_eq!(ep.query_params[1].name, "overwrite");
1162        // Body still works:
1163        assert_eq!(ep.body_type, "SecretWriteRequest");
1164    }
1165}
1166
1167#[cfg(test)]
1168mod path_param_extraction_tests {
1169    use super::extract_path_param_names;
1170
1171    #[test]
1172    fn empty_path_no_placeholders() {
1173        assert_eq!(extract_path_param_names("/api/users"), Ok(vec![]));
1174        assert_eq!(extract_path_param_names("/"), Ok(vec![]));
1175        assert_eq!(extract_path_param_names(""), Ok(vec![]));
1176    }
1177
1178    #[test]
1179    fn single_placeholder() {
1180        assert_eq!(
1181            extract_path_param_names("/api/users/{id}"),
1182            Ok(vec!["id".to_string()])
1183        );
1184    }
1185
1186    #[test]
1187    fn multiple_placeholders_in_declaration_order() {
1188        assert_eq!(
1189            extract_path_param_names(
1190                "/api/tenants/{tenant_id}/secrets/{secret_name}"
1191            ),
1192            Ok(vec![
1193                "tenant_id".to_string(),
1194                "secret_name".to_string(),
1195            ])
1196        );
1197    }
1198
1199    #[test]
1200    fn kivi_chat_history_path_pattern() {
1201        // The exact pattern the kivi adopter reported (2026-05-20):
1202        // POST /api/tenants/{tenant_id}/secrets/{secret_name}
1203        // Both names extracted in source order.
1204        let names = extract_path_param_names(
1205            "/api/tenants/{tenant_id}/secrets/{secret_name}",
1206        );
1207        assert_eq!(
1208            names,
1209            Ok(vec![
1210                "tenant_id".to_string(),
1211                "secret_name".to_string(),
1212            ])
1213        );
1214    }
1215
1216    #[test]
1217    fn duplicate_placeholder_returns_err() {
1218        assert_eq!(
1219            extract_path_param_names("/api/users/{id}/posts/{id}"),
1220            Err("id".to_string())
1221        );
1222    }
1223
1224    #[test]
1225    fn underscore_and_numeric_in_name() {
1226        assert_eq!(
1227            extract_path_param_names("/api/{user_id}/items/{item_2}"),
1228            Ok(vec!["user_id".to_string(), "item_2".to_string()])
1229        );
1230    }
1231
1232    #[test]
1233    fn leading_underscore_accepted() {
1234        // Identifiers in HTTP paths often start with letters but the
1235        // grammar permits leading underscore (parity with Rust identifier
1236        // rules). The flow parameter name on the binding side has to
1237        // match exactly, so adopters with `_internal_id` in the path
1238        // can pair it with a same-named flow param.
1239        assert_eq!(
1240            extract_path_param_names("/api/{_internal}"),
1241            Ok(vec!["_internal".to_string()])
1242        );
1243    }
1244
1245    #[test]
1246    fn malformed_placeholder_silently_ignored() {
1247        // Content inside `{...}` that does not match the identifier
1248        // grammar is skipped at this layer. axum surfaces the route
1249        // registration failure if the literal text is invalid.
1250        assert_eq!(
1251            extract_path_param_names("/api/{not valid}"),
1252            Ok(vec![])
1253        );
1254        // Empty braces — same: skip silently.
1255        assert_eq!(extract_path_param_names("/api/{}"), Ok(vec![]));
1256        // Mixed: malformed brace skipped, valid placeholder kept.
1257        assert_eq!(
1258            extract_path_param_names("/api/{tenant id}/users/{id}"),
1259            Ok(vec!["id".to_string()])
1260        );
1261    }
1262
1263    #[test]
1264    fn unterminated_brace_returns_clean() {
1265        // Open brace with no close brace — give up without panicking.
1266        // (axum surfaces the malformed-route error at registration.)
1267        assert_eq!(extract_path_param_names("/api/{id"), Ok(vec![]));
1268    }
1269
1270    #[test]
1271    fn placeholders_at_path_boundaries() {
1272        // Placeholder as the very first segment AND the very last
1273        // segment — both should be extracted.
1274        assert_eq!(
1275            extract_path_param_names("{prefix}/api/users/{id}"),
1276            Ok(vec!["prefix".to_string(), "id".to_string()])
1277        );
1278        assert_eq!(
1279            extract_path_param_names("/api/{id}"),
1280            Ok(vec!["id".to_string()])
1281        );
1282    }
1283
1284    #[test]
1285    fn deduplication_detects_non_adjacent_duplicates() {
1286        // The duplicate-detection sweep is global, not just adjacent.
1287        assert_eq!(
1288            extract_path_param_names(
1289                "/api/orgs/{org_id}/teams/{team_id}/repos/{org_id}"
1290            ),
1291            Err("org_id".to_string())
1292        );
1293    }
1294
1295    #[test]
1296    fn never_panics_on_arbitrary_input() {
1297        // Light fuzz: a handful of weird inputs return cleanly.
1298        for input in &[
1299            "{",
1300            "}",
1301            "{}",
1302            "{{}}",
1303            "{{{",
1304            "/api/{}/{id}",
1305            "////",
1306            "\u{1F4A1}",        // emoji (lightbulb)
1307            "\u{0000}",         // null byte
1308        ] {
1309            let _ = extract_path_param_names(input); // must not panic
1310        }
1311    }
1312}
1313
1314#[cfg(test)]
1315mod capability_slug_tests {
1316    use super::is_valid_capability_slug;
1317
1318    #[test]
1319    fn accepts_canonical_examples() {
1320        assert!(is_valid_capability_slug("admin"));
1321        assert!(is_valid_capability_slug("legal.read"));
1322        assert!(is_valid_capability_slug("hipaa.phi.read"));
1323        assert!(is_valid_capability_slug("bank.officer.senior"));
1324        assert!(is_valid_capability_slug("a"));
1325        assert!(is_valid_capability_slug("a_b"));
1326        assert!(is_valid_capability_slug("a1"));
1327        assert!(is_valid_capability_slug("a.b1_c"));
1328    }
1329
1330    #[test]
1331    fn rejects_empty_string() {
1332        assert!(!is_valid_capability_slug(""));
1333    }
1334
1335    #[test]
1336    fn rejects_uppercase() {
1337        assert!(!is_valid_capability_slug("Admin"));
1338        assert!(!is_valid_capability_slug("admin.READ"));
1339    }
1340
1341    #[test]
1342    fn rejects_digit_first() {
1343        assert!(!is_valid_capability_slug("1admin"));
1344        assert!(!is_valid_capability_slug("admin.1read"));
1345    }
1346
1347    #[test]
1348    fn rejects_hyphen() {
1349        assert!(!is_valid_capability_slug("bank-officer"));
1350    }
1351
1352    #[test]
1353    fn rejects_empty_segments() {
1354        assert!(!is_valid_capability_slug("bank..a"));
1355        assert!(!is_valid_capability_slug(".admin"));
1356        assert!(!is_valid_capability_slug("admin."));
1357    }
1358
1359    #[test]
1360    fn rejects_special_chars() {
1361        assert!(!is_valid_capability_slug("admin@read"));
1362        assert!(!is_valid_capability_slug("admin/read"));
1363        assert!(!is_valid_capability_slug("admin read"));
1364    }
1365}
1366
1367// ── Parser ───────────────────────────────────────────────────────────────────
1368
1369pub struct Parser {
1370    tokens: Vec<Token>,
1371    pos: usize,
1372    /// Fase 14.a — leading trivia parallel array, indexed by the
1373    /// effective-token position. `leading_trivia[i]` is the comment
1374    /// trivia that appeared between the previous effective token (or
1375    /// file start) and `tokens[i]`.
1376    leading_trivia: Vec<Vec<Trivia>>,
1377    /// Fase 14.a — trailing trivia parallel array. `trailing_trivia[i]`
1378    /// is the comment trivia on the same line as `tokens[i]`, before
1379    /// the next effective token. Populated by the constructor.
1380    trailing_trivia: Vec<Vec<Trivia>>,
1381    /// Fase 17.a — side-channel for tagging let value_kind. Set by
1382    /// `parse_let_atom` / `parse_let_value_expr` as they descend; read
1383    /// at the end of `parse_let` and stored on the LetStatement.
1384    last_let_value_kind: String,
1385    /// Fase 19.e — loop nesting depth for break/continue scope check.
1386    /// Incremented at the start of `parse_for_in`, decremented after.
1387    /// `parse_break`/`parse_continue` raise ParseError when this is
1388    /// zero (the keyword has no meaning outside a loop body).
1389    loop_depth: u32,
1390    /// §Fase 28.d — Optional source text + filename for the rustc-
1391    /// style source-context block on `ParseError`. Set via the
1392    /// fluent `Parser::with_source` builder; default `None` keeps
1393    /// existing callers (`Parser::new(tokens).parse()`) emitting
1394    /// the legacy single-line shape.
1395    source: Option<String>,
1396    filename: String,
1397}
1398
1399impl Parser {
1400    pub fn new(raw_tokens: Vec<Token>) -> Self {
1401        // ── Fase 14.a — split the raw token stream into:
1402        //   - effective tokens the grammar consumes (cursor advances
1403        //     over these as before),
1404        //   - parallel `leading_trivia` / `trailing_trivia` arrays
1405        //     indexed by effective-token position.
1406        // Comments on a fresh line attach as leading trivia of the
1407        // next effective token; comments on the same line as an
1408        // effective token attach as trailing trivia of that token.
1409        // Roslyn/Swift convention.
1410        let mut effective: Vec<Token> = Vec::with_capacity(raw_tokens.len());
1411        let mut leading: Vec<Vec<Trivia>> = Vec::with_capacity(raw_tokens.len());
1412        let mut trailing: Vec<Vec<Trivia>> = Vec::with_capacity(raw_tokens.len());
1413
1414        let mut pending_leading: Vec<Trivia> = Vec::new();
1415        let mut last_effective_line: i64 = -1;
1416        for tok in raw_tokens {
1417            if is_comment_token(&tok.ttype) {
1418                let kind = token_to_trivia_kind(&tok.ttype)
1419                    .expect("comment token must map to a trivia kind");
1420                let triv = Trivia {
1421                    kind,
1422                    text: tok.value,
1423                    line: tok.line,
1424                    column: tok.column,
1425                };
1426                if !effective.is_empty() && (tok.line as i64) == last_effective_line {
1427                    trailing.last_mut().unwrap().push(triv);
1428                } else {
1429                    pending_leading.push(triv);
1430                }
1431            } else {
1432                last_effective_line = tok.line as i64;
1433                effective.push(tok);
1434                leading.push(std::mem::take(&mut pending_leading));
1435                trailing.push(Vec::new());
1436            }
1437        }
1438
1439        Parser {
1440            tokens: effective,
1441            pos: 0,
1442            leading_trivia: leading,
1443            trailing_trivia: trailing,
1444            last_let_value_kind: "literal".to_string(),
1445            loop_depth: 0,
1446            source: None,
1447            filename: "<source>".to_string(),
1448        }
1449    }
1450
1451    /// §Fase 28.d — Fluent attach of source text + filename for
1452    /// rustc-style source-context blocks on emitted `ParseError`s.
1453    /// Returns `self` so it chains with `.parse_with_recovery()`:
1454    ///
1455    /// ```ignore
1456    /// let result = Parser::new(tokens)
1457    ///     .with_source(src, "foo.axon")
1458    ///     .parse_with_recovery();
1459    /// ```
1460    ///
1461    /// No-op of any other behaviour — pure metadata attach.
1462    #[must_use]
1463    pub fn with_source(mut self, source: &str, filename: &str) -> Self {
1464        self.source = Some(source.to_string());
1465        self.filename = filename.to_string();
1466        self
1467    }
1468
1469    // ── public API ───────────────────────────────────────────────
1470
1471    pub fn parse(&mut self) -> Result<Program, ParseError> {
1472        let mut program = Program {
1473            declarations: Vec::new(),
1474            declaration_trivia: Vec::new(),
1475            loc: Loc { line: 1, column: 1 },
1476        };
1477        while !self.check(TokenType::Eof) {
1478            // Capture trivia around the declaration. `start_pos` is
1479            // the effective-token position of the declaration's first
1480            // token; that position carries the leading trivia. After
1481            // parsing, `pos - 1` is the last token consumed; that
1482            // position carries the trailing trivia.
1483            let start_pos = self.pos;
1484            let mut decl = match self.parse_declaration() {
1485                Ok(d) => d,
1486                Err(e) => return Err(self.attach_source_to_error(e)),
1487            };
1488            let end_pos = self.pos.saturating_sub(1);
1489            let leading = self
1490                .leading_trivia
1491                .get(start_pos)
1492                .cloned()
1493                .unwrap_or_default();
1494            let trailing = self
1495                .trailing_trivia
1496                .get(end_pos)
1497                .cloned()
1498                .unwrap_or_default();
1499            // Fase 14.b — also copy trivia into the per-struct fields on
1500            // the declaration so consumers can read `flow.leading_trivia`
1501            // directly without going through `program.declaration_trivia[i]`.
1502            // The side-channel is preserved for backward compat with
1503            // 14.a callers and as a flat enumeration source.
1504            attach_trivia_to_decl(&mut decl, leading.clone(), trailing.clone());
1505            program.declarations.push(decl);
1506            program
1507                .declaration_trivia
1508                .push(DeclarationTrivia { leading, trailing });
1509        }
1510        Ok(program)
1511    }
1512
1513    // ── §Fase 28.c — recovery-mode parse ─────────────────────────
1514    //
1515    // Mirror of Python's `Parser.parse_with_recovery` from
1516    // `axon/compiler/parser.py`. Wraps `parse_declaration` in a
1517    // try/recover loop: on any `ParseError` the error is appended to
1518    // the list and the cursor advances to the next sync point, then
1519    // parsing resumes. The two stacks must produce structurally
1520    // identical error lists on the same input — that is the cross-
1521    // stack drift gate (D7). See the test module
1522    // `tests::fase28_recovery_tests` and Python-side
1523    // `tests/test_fase28_parser_recovery.py`.
1524
1525    /// Recovery-mode parse. Collects every parse error in source
1526    /// order; the existing `parse()` API remains fail-fast (D9).
1527    ///
1528    /// # Recovery contract (D2)
1529    ///
1530    /// On `ParseError`:
1531    ///   1. Push the error onto `errors`.
1532    ///   2. If the cursor is already on a top-level declaration
1533    ///      keyword (and brace-depth ≤ 0), do not consume — the
1534    ///      caller should retry the declaration parse from here.
1535    ///      Otherwise advance one token to make progress, then
1536    ///      walk to the next sync point.
1537    ///   3. Resume the outer loop.
1538    ///
1539    /// Sync points: top-level declaration keyword at brace-depth ≤ 0,
1540    /// or EOF. Negative depths are treated identically to ≤ 0 — the
1541    /// walker keeps walking through over-balanced `}` rather than
1542    /// pretending a closing brace is itself a sync point (which would
1543    /// emit a ghost "Unexpected token at top level" error in the
1544    /// outer loop).
1545    pub fn parse_with_recovery(&mut self) -> ParseResult {
1546        let mut program = Program {
1547            declarations: Vec::new(),
1548            declaration_trivia: Vec::new(),
1549            loc: Loc { line: 1, column: 1 },
1550        };
1551        let mut errors: Vec<ParseError> = Vec::new();
1552
1553        while !self.check(TokenType::Eof) {
1554            let start_pos = self.pos;
1555            match self.parse_declaration() {
1556                Ok(mut decl) => {
1557                    let end_pos = self.pos.saturating_sub(1);
1558                    let leading = self
1559                        .leading_trivia
1560                        .get(start_pos)
1561                        .cloned()
1562                        .unwrap_or_default();
1563                    let trailing = self
1564                        .trailing_trivia
1565                        .get(end_pos)
1566                        .cloned()
1567                        .unwrap_or_default();
1568                    attach_trivia_to_decl(&mut decl, leading.clone(), trailing.clone());
1569                    program.declarations.push(decl);
1570                    program
1571                        .declaration_trivia
1572                        .push(DeclarationTrivia { leading, trailing });
1573                }
1574                Err(err) => {
1575                    // §Fase 28.d — attach source-context block when a
1576                    // source has been provided via `with_source(...)`;
1577                    // otherwise the error keeps its single-line shape.
1578                    errors.push(self.attach_source_to_error(err));
1579                    // Make progress. If parse_declaration returned
1580                    // immediately on the same token (e.g. unknown
1581                    // top-level token), we MUST advance at least one
1582                    // token to avoid an infinite loop.
1583                    if self.pos == start_pos && !self.check(TokenType::Eof) {
1584                        self.advance();
1585                    }
1586                    self.advance_to_sync_point();
1587                }
1588            }
1589        }
1590
1591        ParseResult { program, errors }
1592    }
1593
1594    /// §Fase 28.d — Decorate a `ParseError` with a `SourceSnippet`
1595    /// when the parser has source context attached, otherwise return
1596    /// the error unchanged. Idempotent: if the error already carries
1597    /// a snippet, this overwrites it with the parser's source.
1598    fn attach_source_to_error(&self, err: ParseError) -> ParseError {
1599        match &self.source {
1600            Some(src) if err.line >= 1 => err.attach_source(src, &self.filename),
1601            _ => err,
1602        }
1603    }
1604
1605    /// §Fase 28.c — Walk the cursor forward until the next sync
1606    /// point (top-level declaration keyword at brace-depth ≤ 0) or
1607    /// EOF. Used by `parse_with_recovery` to skip the malformed
1608    /// remainder of a failed declaration.
1609    fn advance_to_sync_point(&mut self) {
1610        let mut depth: i32 = 0;
1611        while !self.check(TokenType::Eof) {
1612            let tt = self.current().ttype.clone();
1613            // Sync at top-level keywords when depth ≤ 0. We do not
1614            // consume the keyword — the outer loop will dispatch on
1615            // it.
1616            if is_top_level_decl_kw_for_recovery(&tt) && depth <= 0 {
1617                return;
1618            }
1619            if matches!(tt, TokenType::LBrace) {
1620                depth += 1;
1621            } else if matches!(tt, TokenType::RBrace) {
1622                depth -= 1;
1623            }
1624            self.advance();
1625        }
1626    }
1627
1628    // ── token helpers ────────────────────────────────────────────
1629
1630    fn current(&self) -> &Token {
1631        if self.pos >= self.tokens.len() {
1632            self.tokens.last().unwrap() // EOF sentinel
1633        } else {
1634            &self.tokens[self.pos]
1635        }
1636    }
1637
1638    fn advance(&mut self) -> &Token {
1639        let idx = self.pos;
1640        if self.pos < self.tokens.len() {
1641            self.pos += 1;
1642        }
1643        &self.tokens[idx]
1644    }
1645
1646    fn check(&self, tt: TokenType) -> bool {
1647        self.current().ttype == tt
1648    }
1649
1650    fn consume(&mut self, expected: TokenType) -> Result<Token, ParseError> {
1651        let tok = self.current().clone();
1652        if tok.ttype != expected {
1653            return Err(ParseError {
1654                message: format!(
1655                    "Expected {:?}, found {:?}('{}')",
1656                    expected, tok.ttype, tok.value
1657                ),
1658                line: tok.line,
1659                column: tok.column,
1660                            ..Default::default()
1661            });
1662        }
1663        self.pos += 1;
1664        Ok(tok)
1665    }
1666
1667    /// §Fase 41.b — build a `ParseError` at the current token's location.
1668    fn error(&self, message: &str) -> ParseError {
1669        let tok = self.current();
1670        ParseError { message: message.to_string(), line: tok.line, column: tok.column, ..Default::default() }
1671    }
1672
1673    /// Consume any identifier or keyword-used-as-value.
1674    fn consume_any_ident_or_kw(&mut self) -> Result<Token, ParseError> {
1675        let tok = self.current().clone();
1676        match tok.ttype {
1677            TokenType::Identifier
1678            | TokenType::Bool
1679            | TokenType::StringLit
1680            | TokenType::Integer
1681            | TokenType::Float => {
1682                self.pos += 1;
1683                Ok(tok)
1684            }
1685            _ => {
1686                // Allow any keyword token whose value is alphabetic
1687                if !tok.value.is_empty()
1688                    && tok.value.chars().all(|c| c.is_alphanumeric() || c == '_')
1689                    && tok.ttype != TokenType::Eof
1690                {
1691                    self.pos += 1;
1692                    Ok(tok)
1693                } else {
1694                    Err(ParseError {
1695                        message: format!(
1696                            "Expected identifier or keyword value, found {:?}('{}')",
1697                            tok.ttype, tok.value
1698                        ),
1699                        line: tok.line,
1700                        column: tok.column,
1701                                            ..Default::default()
1702                    })
1703                }
1704            }
1705        }
1706    }
1707
1708    fn consume_number(&mut self) -> Result<f64, ParseError> {
1709        let tok = self.current().clone();
1710        match tok.ttype {
1711            TokenType::Float | TokenType::Integer => {
1712                self.pos += 1;
1713                tok.value.parse::<f64>().map_err(|_| ParseError {
1714                    message: format!("Invalid number '{}'", tok.value),
1715                    line: tok.line,
1716                    column: tok.column,
1717                                    ..Default::default()
1718                })
1719            }
1720            _ => Err(ParseError {
1721                message: format!("Expected number, found {:?}('{}')", tok.ttype, tok.value),
1722                line: tok.line,
1723                column: tok.column,
1724                            ..Default::default()
1725            }),
1726        }
1727    }
1728
1729    fn parse_bool(&mut self) -> Result<bool, ParseError> {
1730        let tok = self.consume(TokenType::Bool)?;
1731        Ok(tok.value == "true")
1732    }
1733
1734    fn loc_of(&self, tok: &Token) -> Loc {
1735        Loc {
1736            line: tok.line,
1737            column: tok.column,
1738        }
1739    }
1740
1741    fn check_comparison(&self) -> bool {
1742        matches!(
1743            self.current().ttype,
1744            TokenType::Lt
1745                | TokenType::Gt
1746                | TokenType::Lte
1747                | TokenType::Gte
1748                | TokenType::Eq
1749                | TokenType::Neq
1750        )
1751    }
1752
1753    fn check_run_modifier(&self) -> bool {
1754        matches!(
1755            self.current().ttype,
1756            TokenType::As
1757                | TokenType::Within
1758                | TokenType::ConstrainedBy
1759                | TokenType::OnFailure
1760                | TokenType::OutputTo
1761                | TokenType::Effort
1762        )
1763    }
1764
1765    // ── list helpers ─────────────────────────────────────────────
1766
1767    fn parse_string_list(&mut self) -> Result<Vec<String>, ParseError> {
1768        self.consume(TokenType::LBracket)?;
1769        let mut items = Vec::new();
1770        items.push(self.consume(TokenType::StringLit)?.value);
1771        while self.check(TokenType::Comma) {
1772            self.advance();
1773            items.push(self.consume(TokenType::StringLit)?.value);
1774        }
1775        self.consume(TokenType::RBracket)?;
1776        Ok(items)
1777    }
1778
1779    fn parse_identifier_list(&mut self) -> Result<Vec<String>, ParseError> {
1780        let mut names = Vec::new();
1781        names.push(self.consume(TokenType::Identifier)?.value);
1782        while self.check(TokenType::Comma) {
1783            self.advance();
1784            names.push(self.consume(TokenType::Identifier)?.value);
1785        }
1786        Ok(names)
1787    }
1788
1789    fn parse_bracketed_identifiers(&mut self) -> Result<Vec<String>, ParseError> {
1790        self.consume(TokenType::LBracket)?;
1791        let items = self.parse_extended_identifier_list()?;
1792        self.consume(TokenType::RBracket)?;
1793        Ok(items)
1794    }
1795
1796    fn parse_extended_identifier_list(&mut self) -> Result<Vec<String>, ParseError> {
1797        let mut items = Vec::new();
1798        items.push(self.consume_any_ident_or_kw()?.value);
1799        while self.check(TokenType::Comma) {
1800            self.advance();
1801            items.push(self.consume_any_ident_or_kw()?.value);
1802        }
1803        Ok(items)
1804    }
1805
1806    fn parse_dotted_identifier(&mut self) -> Result<String, ParseError> {
1807        let mut parts = vec![self.consume_any_ident_or_kw()?.value];
1808        while self.check(TokenType::Dot) {
1809            self.advance();
1810            parts.push(self.consume_any_ident_or_kw()?.value);
1811        }
1812        Ok(parts.join("."))
1813    }
1814
1815    fn parse_expression_string(&mut self) -> Result<String, ParseError> {
1816        if self.check(TokenType::LBracket) {
1817            let items = self.parse_bracketed_dot_identifiers()?;
1818            return Ok(format!("[{}]", items.join(", ")));
1819        }
1820        self.parse_dotted_identifier()
1821    }
1822
1823    fn parse_bracketed_dot_identifiers(&mut self) -> Result<Vec<String>, ParseError> {
1824        self.consume(TokenType::LBracket)?;
1825        let mut items = vec![self.parse_dotted_identifier()?];
1826        while self.check(TokenType::Comma) {
1827            self.advance();
1828            items.push(self.parse_dotted_identifier()?);
1829        }
1830        self.consume(TokenType::RBracket)?;
1831        Ok(items)
1832    }
1833
1834    fn parse_argument_list(&mut self) -> Result<Vec<String>, ParseError> {
1835        let mut args = Vec::new();
1836        while !self.check(TokenType::RParen) {
1837            let tok = self.current().clone();
1838            match tok.ttype {
1839                TokenType::StringLit | TokenType::Integer | TokenType::Float => {
1840                    self.advance();
1841                    args.push(tok.value);
1842                }
1843                TokenType::Identifier => {
1844                    self.advance();
1845                    let mut val = tok.value;
1846                    if self.check(TokenType::Dot) {
1847                        self.advance();
1848                        val.push('.');
1849                        val.push_str(&self.consume_any_ident_or_kw()?.value);
1850                    }
1851                    args.push(val);
1852                }
1853                _ => {
1854                    self.advance();
1855                    let key = tok.value;
1856                    if self.check(TokenType::Colon) {
1857                        self.advance();
1858                        let v = self.advance().value.clone();
1859                        args.push(format!("{key}:{v}"));
1860                    } else {
1861                        args.push(key);
1862                    }
1863                }
1864            }
1865            if self.check(TokenType::Comma) {
1866                self.advance();
1867            }
1868        }
1869        Ok(args)
1870    }
1871
1872    /// Skip a single value or balanced bracketed/braced block (unknown field).
1873    fn skip_value(&mut self) {
1874        match self.current().ttype {
1875            TokenType::LBracket => {
1876                self.advance();
1877                let mut depth = 1u32;
1878                while depth > 0 && !self.check(TokenType::Eof) {
1879                    if self.check(TokenType::LBracket) {
1880                        depth += 1;
1881                    } else if self.check(TokenType::RBracket) {
1882                        depth -= 1;
1883                    }
1884                    self.advance();
1885                }
1886            }
1887            TokenType::LBrace => {
1888                self.advance();
1889                let mut depth = 1u32;
1890                while depth > 0 && !self.check(TokenType::Eof) {
1891                    if self.check(TokenType::LBrace) {
1892                        depth += 1;
1893                    } else if self.check(TokenType::RBrace) {
1894                        depth -= 1;
1895                    }
1896                    self.advance();
1897                }
1898            }
1899            TokenType::Lt => {
1900                // effect row: <io, network, ...>
1901                self.advance();
1902                let mut depth = 1u32;
1903                while depth > 0 && !self.check(TokenType::Eof) {
1904                    if self.check(TokenType::Lt) {
1905                        depth += 1;
1906                    } else if self.check(TokenType::Gt) {
1907                        depth -= 1;
1908                    }
1909                    self.advance();
1910                }
1911            }
1912            _ => {
1913                self.advance();
1914                while self.check(TokenType::Dot) {
1915                    self.advance();
1916                    self.advance();
1917                }
1918            }
1919        }
1920    }
1921
1922    /// Skip a balanced `{ ... }` block including its braces.
1923    fn skip_braced_block(&mut self) -> Result<(), ParseError> {
1924        self.consume(TokenType::LBrace)?;
1925        let mut depth = 1u32;
1926        while depth > 0 {
1927            if self.check(TokenType::Eof) {
1928                let tok = self.current();
1929                return Err(ParseError {
1930                    message: "Unterminated block — expected '}'".to_string(),
1931                    line: tok.line,
1932                    column: tok.column,
1933                                    ..Default::default()
1934                });
1935            }
1936            if self.check(TokenType::LBrace) {
1937                depth += 1;
1938            } else if self.check(TokenType::RBrace) {
1939                depth -= 1;
1940            }
1941            self.advance();
1942        }
1943        Ok(())
1944    }
1945
1946    fn at_declaration_start(&self) -> bool {
1947        is_declaration_keyword(&self.current().ttype) || self.check(TokenType::Eof)
1948    }
1949
1950    // ── top-level dispatch ───────────────────────────────────────
1951
1952    fn parse_declaration(&mut self) -> Result<Declaration, ParseError> {
1953        let tok = self.current().clone();
1954
1955        match tok.ttype {
1956            TokenType::Import => self.parse_import().map(Declaration::Import),
1957            TokenType::Persona => self.parse_persona().map(Declaration::Persona),
1958            TokenType::Context => self.parse_context().map(Declaration::Context),
1959            TokenType::Anchor => self.parse_anchor().map(Declaration::Anchor),
1960            TokenType::Memory => self.parse_memory().map(Declaration::Memory),
1961            TokenType::Tool => self.parse_tool().map(Declaration::Tool),
1962            TokenType::Type => self.parse_type_def().map(Declaration::Type),
1963            TokenType::Flow => self.parse_flow().map(Declaration::Flow),
1964            TokenType::Intent => self.parse_intent().map(Declaration::Intent),
1965            TokenType::Run => self.parse_run().map(Declaration::Run),
1966            TokenType::Let => self.parse_let().map(Declaration::Let),
1967            TokenType::Know | TokenType::Believe | TokenType::Speculate | TokenType::Doubt => {
1968                self.parse_epistemic_block().map(Declaration::Epistemic)
1969            }
1970            TokenType::Lambda => self.parse_lambda_data().map(Declaration::LambdaData),
1971
1972            // ── Tier 2 declarations (full AST) ──────────────────
1973            TokenType::Agent => self.parse_agent().map(Declaration::Agent),
1974            TokenType::Shield => self.parse_shield().map(Declaration::Shield),
1975            TokenType::Pix => self.parse_pix().map(Declaration::Pix),
1976            TokenType::Ledger => self.parse_ledger().map(Declaration::Ledger),
1977            TokenType::Psyche => self.parse_psyche().map(Declaration::Psyche),
1978            TokenType::Corpus => self.parse_corpus().map(Declaration::Corpus),
1979            TokenType::Dataspace => self.parse_dataspace().map(Declaration::Dataspace),
1980            TokenType::Ots => self.parse_ots().map(Declaration::Ots),
1981            TokenType::Mandate => self.parse_mandate().map(Declaration::Mandate),
1982            TokenType::Compute => self.parse_compute().map(Declaration::Compute),
1983            TokenType::Daemon => self.parse_daemon().map(Declaration::Daemon),
1984            TokenType::Extension => self.parse_extension().map(Declaration::Extension),
1985            TokenType::AxonStore => self.parse_axonstore().map(Declaration::AxonStore),
1986            TokenType::AxonEndpoint => self.parse_axonendpoint().map(Declaration::AxonEndpoint),
1987
1988            // ── §λ-L-E Fase 1 — I/O cognitivo ───────────────────
1989            TokenType::Resource => self.parse_resource().map(Declaration::Resource),
1990            TokenType::Fabric => self.parse_fabric().map(Declaration::Fabric),
1991            TokenType::Manifest => self.parse_manifest().map(Declaration::Manifest),
1992            TokenType::Observe => self.parse_observe().map(Declaration::Observe),
1993
1994            // ── §λ-L-E Fase 3 — Control cognitivo ───────────────
1995            TokenType::Reconcile => self.parse_reconcile().map(Declaration::Reconcile),
1996            TokenType::Lease => self.parse_lease().map(Declaration::Lease),
1997            TokenType::Ensemble => self.parse_ensemble().map(Declaration::Ensemble),
1998
1999            // ── §λ-L-E Fase 4 — Topology + π-calculus sessions ─
2000            TokenType::Session => self.parse_session_definition().map(Declaration::Session),
2001            TokenType::Topology => self.parse_topology().map(Declaration::Topology),
2002
2003            // ── §Fase 41.b — typed WebSocket transport ─────────
2004            TokenType::Socket => self.parse_socket().map(Declaration::Socket),
2005
2006            // ── §Fase 51.c.2 — Pauli-sum observable ────────────
2007            TokenType::Observable => self.parse_observable().map(Declaration::Observable),
2008
2009            // ── §Fase 69.a — Advantage Witness ──────────────────
2010            TokenType::Witness => self.parse_witness().map(Declaration::Witness),
2011
2012            // ── §λ-L-E Fase 5 — Cognitive immune system ─────────
2013            TokenType::Immune => self.parse_immune().map(Declaration::Immune),
2014            TokenType::Reflex => self.parse_reflex().map(Declaration::Reflex),
2015            TokenType::Heal => self.parse_heal().map(Declaration::Heal),
2016
2017            // ── §λ-L-E Fase 9 — UI cognitiva ────────────────────
2018            TokenType::Component => self.parse_component().map(Declaration::Component),
2019            TokenType::View => self.parse_view().map(Declaration::View),
2020
2021            // ── §λ-L-E Fase 13 — Mobile typed channels ──────────
2022            TokenType::Channel => self.parse_channel().map(Declaration::Channel),
2023
2024            // ── Tier 3+ structural fallback ─────────────────────
2025            // Store operations: keyword target { ... } or keyword target ...
2026            TokenType::Ingest
2027            | TokenType::Persist
2028            | TokenType::Retrieve
2029            | TokenType::Mutate
2030            | TokenType::Purge
2031            | TokenType::Transact => self.parse_generic_declaration(),
2032
2033            // MCP declaration
2034            TokenType::Mcp => self.parse_generic_declaration(),
2035
2036            _ => {
2037                // §Fase 28.e — append "Did you mean X?" hint when the
2038                // unknown token looks like a typo'd top-level keyword
2039                // (Levenshtein ≤ 2). D3, D11 ratified 2026-05-10.
2040                let hint = crate::smart_suggest::suggest_for(
2041                    &tok.value,
2042                    crate::smart_suggest::TOP_LEVEL_KEYWORD_NAMES,
2043                );
2044                let base = format!(
2045                    "Unexpected token at top level: '{}' — expected declaration \
2046                     (persona, context, anchor, flow, run, ...)",
2047                    tok.value
2048                );
2049                let message = if hint.is_empty() {
2050                    base
2051                } else {
2052                    format!("{base}. {hint}")
2053                };
2054                Err(ParseError {
2055                    message,
2056                    line: tok.line,
2057                    column: tok.column,
2058                    ..Default::default()
2059                })
2060            }
2061        }
2062    }
2063
2064    // ── IMPORT ───────────────────────────────────────────────────
2065
2066    fn parse_import(&mut self) -> Result<ImportNode, ParseError> {
2067        let tok = self.consume(TokenType::Import)?;
2068        let loc = self.loc_of(&tok);
2069
2070        let mut path_parts = Vec::new();
2071
2072        // Optional @ scope
2073        if self.check(TokenType::At) {
2074            self.advance();
2075            let first = self.consume(TokenType::Identifier)?;
2076            path_parts.push(format!("@{}", first.value));
2077        } else {
2078            let first = self.consume(TokenType::Identifier)?;
2079            path_parts.push(first.value);
2080        }
2081
2082        while self.check(TokenType::Dot) {
2083            self.advance();
2084            if self.check(TokenType::LBrace) {
2085                break;
2086            }
2087            let part = self.consume(TokenType::Identifier)?;
2088            path_parts.push(part.value);
2089        }
2090
2091        let mut names = Vec::new();
2092        if self.check(TokenType::LBrace) {
2093            self.advance();
2094            names = self.parse_identifier_list()?;
2095            self.consume(TokenType::RBrace)?;
2096        }
2097
2098        // Skip optional APX policy (with apx { ... })
2099        if self.current().value == "with" {
2100            self.advance();
2101            self.advance(); // consume "apx"
2102            if self.check(TokenType::LBrace) {
2103                self.skip_braced_block()?;
2104            }
2105        }
2106
2107        Ok(ImportNode {
2108            module_path: path_parts,
2109            names,
2110            loc,
2111            leading_trivia: Vec::new(),
2112            trailing_trivia: Vec::new(),
2113        })
2114    }
2115
2116    // ── PERSONA ──────────────────────────────────────────────────
2117
2118    fn parse_persona(&mut self) -> Result<PersonaDefinition, ParseError> {
2119        let tok = self.consume(TokenType::Persona)?;
2120        let loc = self.loc_of(&tok);
2121        let name = self.consume(TokenType::Identifier)?.value;
2122        self.consume(TokenType::LBrace)?;
2123
2124        let mut node = PersonaDefinition {
2125            name,
2126            domain: Vec::new(),
2127            tone: String::new(),
2128            confidence_threshold: None,
2129            cite_sources: None,
2130            refuse_if: Vec::new(),
2131            language: String::new(),
2132            description: String::new(),
2133            loc,
2134            leading_trivia: Vec::new(),
2135            trailing_trivia: Vec::new(),
2136        };
2137
2138        while !self.check(TokenType::RBrace) {
2139            let field_name = self.current().value.clone();
2140            self.advance();
2141            self.consume(TokenType::Colon)?;
2142
2143            match field_name.as_str() {
2144                "domain" => node.domain = self.parse_string_list()?,
2145                "tone" => node.tone = self.consume_any_ident_or_kw()?.value,
2146                "confidence_threshold" => node.confidence_threshold = Some(self.consume_number()?),
2147                "cite_sources" => node.cite_sources = Some(self.parse_bool()?),
2148                "refuse_if" => node.refuse_if = self.parse_bracketed_identifiers()?,
2149                "language" => node.language = self.consume(TokenType::StringLit)?.value,
2150                "description" => node.description = self.consume(TokenType::StringLit)?.value,
2151                _ => self.skip_value(),
2152            }
2153        }
2154        self.consume(TokenType::RBrace)?;
2155        Ok(node)
2156    }
2157
2158    // ── CONTEXT ──────────────────────────────────────────────────
2159
2160    fn parse_context(&mut self) -> Result<ContextDefinition, ParseError> {
2161        let tok = self.consume(TokenType::Context)?;
2162        let loc = self.loc_of(&tok);
2163        let name = self.consume(TokenType::Identifier)?.value;
2164        self.consume(TokenType::LBrace)?;
2165
2166        let mut node = ContextDefinition {
2167            name,
2168            memory_scope: String::new(),
2169            language: String::new(),
2170            depth: String::new(),
2171            max_tokens: None,
2172            temperature: None,
2173            cite_sources: None,
2174            loc,
2175            leading_trivia: Vec::new(),
2176            trailing_trivia: Vec::new(),
2177        };
2178
2179        while !self.check(TokenType::RBrace) {
2180            let field_name = self.current().value.clone();
2181            self.advance();
2182            self.consume(TokenType::Colon)?;
2183
2184            match field_name.as_str() {
2185                "memory" => node.memory_scope = self.consume_any_ident_or_kw()?.value,
2186                "language" => node.language = self.consume(TokenType::StringLit)?.value,
2187                "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
2188                "max_tokens" => {
2189                    node.max_tokens = Some(
2190                        self.consume(TokenType::Integer)?
2191                            .value
2192                            .parse::<i64>()
2193                            .unwrap_or(0),
2194                    )
2195                }
2196                "temperature" => node.temperature = Some(self.consume_number()?),
2197                "cite_sources" => node.cite_sources = Some(self.parse_bool()?),
2198                _ => self.skip_value(),
2199            }
2200        }
2201        self.consume(TokenType::RBrace)?;
2202        Ok(node)
2203    }
2204
2205    // ── ANCHOR ───────────────────────────────────────────────────
2206
2207    fn parse_anchor(&mut self) -> Result<AnchorConstraint, ParseError> {
2208        let tok = self.consume(TokenType::Anchor)?;
2209        let loc = self.loc_of(&tok);
2210        let name = self.consume(TokenType::Identifier)?.value;
2211        self.consume(TokenType::LBrace)?;
2212
2213        let mut node = AnchorConstraint {
2214            name,
2215            require: String::new(),
2216            reject: Vec::new(),
2217            enforce: String::new(),
2218            description: String::new(),
2219            confidence_floor: None,
2220            unknown_response: String::new(),
2221            on_violation: String::new(),
2222            on_violation_target: String::new(),
2223            loc,
2224            leading_trivia: Vec::new(),
2225            trailing_trivia: Vec::new(),
2226        };
2227
2228        while !self.check(TokenType::RBrace) {
2229            let field_name = self.current().value.clone();
2230            self.advance();
2231            self.consume(TokenType::Colon)?;
2232
2233            match field_name.as_str() {
2234                "require" => node.require = self.consume_any_ident_or_kw()?.value,
2235                "description" => node.description = self.consume(TokenType::StringLit)?.value,
2236                "reject" => node.reject = self.parse_bracketed_identifiers()?,
2237                "enforce" => node.enforce = self.consume_any_ident_or_kw()?.value,
2238                "confidence_floor" => node.confidence_floor = Some(self.consume_number()?),
2239                "unknown_response" => {
2240                    node.unknown_response = self.consume(TokenType::StringLit)?.value
2241                }
2242                "on_violation" => {
2243                    // Parse: raise ErrorName | fallback(...) | identifier
2244                    let action = self.consume_any_ident_or_kw()?.value;
2245                    node.on_violation = action.clone();
2246                    if action == "raise" || action == "fallback" {
2247                        node.on_violation_target = self.consume_any_ident_or_kw()?.value;
2248                    }
2249                }
2250                _ => self.skip_value(),
2251            }
2252        }
2253        self.consume(TokenType::RBrace)?;
2254        Ok(node)
2255    }
2256
2257    // ── MEMORY ───────────────────────────────────────────────────
2258
2259    fn parse_memory(&mut self) -> Result<MemoryDefinition, ParseError> {
2260        let tok = self.consume(TokenType::Memory)?;
2261        let loc = self.loc_of(&tok);
2262        let name = self.consume(TokenType::Identifier)?.value;
2263        self.consume(TokenType::LBrace)?;
2264
2265        let mut node = MemoryDefinition {
2266            name,
2267            store: String::new(),
2268            backend: String::new(),
2269            retrieval: String::new(),
2270            decay: String::new(),
2271            loc,
2272            leading_trivia: Vec::new(),
2273            trailing_trivia: Vec::new(),
2274        };
2275
2276        while !self.check(TokenType::RBrace) {
2277            let field_name = self.current().value.clone();
2278            self.advance();
2279            self.consume(TokenType::Colon)?;
2280
2281            match field_name.as_str() {
2282                "store" => node.store = self.consume_any_ident_or_kw()?.value,
2283                "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
2284                "retrieval" => node.retrieval = self.consume_any_ident_or_kw()?.value,
2285                "decay" => {
2286                    if self.check(TokenType::Duration) {
2287                        node.decay = self.advance().value.clone();
2288                    } else {
2289                        node.decay = self.consume_any_ident_or_kw()?.value;
2290                    }
2291                }
2292                _ => self.skip_value(),
2293            }
2294        }
2295        self.consume(TokenType::RBrace)?;
2296        Ok(node)
2297    }
2298
2299    // ── TOOL ─────────────────────────────────────────────────────
2300
2301    fn parse_tool(&mut self) -> Result<ToolDefinition, ParseError> {
2302        let tok = self.consume(TokenType::Tool)?;
2303        let loc = self.loc_of(&tok);
2304        let name = self.consume(TokenType::Identifier)?.value;
2305        self.consume(TokenType::LBrace)?;
2306
2307        let mut node = ToolDefinition {
2308            name,
2309            provider: String::new(),
2310            max_results: None,
2311            filter_expr: String::new(),
2312            timeout: String::new(),
2313            runtime: String::new(),
2314            sandbox: None,
2315            effects: None,
2316            parameters: Vec::new(),
2317            output_type: None,
2318            loc,
2319            leading_trivia: Vec::new(),
2320            trailing_trivia: Vec::new(),
2321        };
2322
2323        while !self.check(TokenType::RBrace) {
2324            let field_name = self.current().value.clone();
2325            self.advance();
2326            self.consume(TokenType::Colon)?;
2327
2328            match field_name.as_str() {
2329                "provider" => node.provider = self.consume_any_ident_or_kw()?.value,
2330                "max_results" => {
2331                    node.max_results = Some(
2332                        self.consume(TokenType::Integer)?
2333                            .value
2334                            .parse::<i64>()
2335                            .unwrap_or(0),
2336                    )
2337                }
2338                "filter" => node.filter_expr = self.parse_filter_expression()?,
2339                "timeout" => node.timeout = self.consume(TokenType::Duration)?.value,
2340                "runtime" => node.runtime = self.consume_any_ident_or_kw()?.value,
2341                "sandbox" => node.sandbox = Some(self.parse_bool()?),
2342                "effects" => node.effects = Some(self.parse_effect_row()?),
2343                // §Fase 58.a — the tool's typed input schema + output type.
2344                "parameters" => node.parameters = self.parse_tool_param_schema()?,
2345                "output_type" => node.output_type = Some(self.parse_output_type_string()?),
2346                _ => self.skip_value(),
2347            }
2348        }
2349        self.consume(TokenType::RBrace)?;
2350        Ok(node)
2351    }
2352
2353    /// §Fase 58.a — parse a tool's INPUT SCHEMA: a brace-delimited list of
2354    /// `name: Type` parameters (`parameters: { query: String, max_results: Int }`).
2355    /// Reuses the flow-parameter shape (`Parameter`), so the same `TypeExpr`
2356    /// grammar — generics like `List<T>`, `?`-optionals — applies. A trailing
2357    /// comma is tolerated; an empty `{}` yields no parameters.
2358    fn parse_tool_param_schema(&mut self) -> Result<Vec<Parameter>, ParseError> {
2359        self.consume(TokenType::LBrace)?;
2360        let mut params = Vec::new();
2361        while !self.check(TokenType::RBrace) {
2362            // Accept a keyword-as-name (`filter`, `type`, `domain`, …) — real
2363            // adopter tool schemas use such parameter names; the `:` after it
2364            // disambiguates.
2365            let name = self.consume_any_ident_or_kw()?;
2366            let ploc = self.loc_of(&name);
2367            self.consume(TokenType::Colon)?;
2368            let type_expr = self.parse_type_expr()?;
2369            params.push(Parameter {
2370                name: name.value,
2371                type_expr,
2372                loc: ploc,
2373            });
2374            if self.check(TokenType::Comma) {
2375                self.advance();
2376            } else {
2377                break;
2378            }
2379        }
2380        self.consume(TokenType::RBrace)?;
2381        Ok(params)
2382    }
2383
2384    fn parse_filter_expression(&mut self) -> Result<String, ParseError> {
2385        let name = self.consume_any_ident_or_kw()?.value;
2386        if self.check(TokenType::LParen) {
2387            self.advance();
2388            let mut parts = vec![name, "(".to_string()];
2389            while !self.check(TokenType::RParen) {
2390                parts.push(self.advance().value.clone());
2391            }
2392            self.consume(TokenType::RParen)?;
2393            parts.push(")".to_string());
2394            Ok(parts.join(""))
2395        } else {
2396            Ok(name)
2397        }
2398    }
2399
2400    fn parse_effect_row(&mut self) -> Result<EffectRow, ParseError> {
2401        let tok = self.consume(TokenType::Lt)?;
2402        let loc = self.loc_of(&tok);
2403        let mut effects = Vec::new();
2404        let mut epistemic_level = String::new();
2405
2406        while !self.check(TokenType::Gt) {
2407            let name = self.consume_any_ident_or_kw()?.value;
2408            if self.check(TokenType::Colon) {
2409                self.advance();
2410                // Fase 11.c / 11.e — qualifiers can be compound slugs
2411                // from a closed catalogue:
2412                //
2413                //   * dot-separated  — `legal:HIPAA.164_502`,
2414                //                       `legal:GDPR.Art6.Consent`,
2415                //                       `legal:PCI_DSS.v4_Req3`
2416                //   * colon-separated — `ots:transform:mulaw8:pcm16`,
2417                //                       `ots:backend:native`
2418                //   * mixed           — supported by the same loop.
2419                //
2420                // The lexer fragments dotted slugs across IDENT /
2421                // INTEGER tokens (e.g., `164_502` lexes as INTEGER
2422                // `164` + IDENT `_502` because `_` starts a fresh
2423                // identifier); we recombine here using source-column
2424                // adjacency so the type checker sees the catalog
2425                // string verbatim.
2426                let level = self.parse_qualifier_value()?;
2427                if name == "epistemic" {
2428                    epistemic_level = level;
2429                } else {
2430                    effects.push(format!("{name}:{level}"));
2431                }
2432            } else {
2433                effects.push(name);
2434            }
2435            if self.check(TokenType::Comma) {
2436                self.advance();
2437            }
2438        }
2439        self.consume(TokenType::Gt)?;
2440
2441        Ok(EffectRow {
2442            effects,
2443            epistemic_level,
2444            loc,
2445        })
2446    }
2447
2448    /// Parse a compound qualifier value following an effect's first
2449    /// colon — supports both dot-separated (`HIPAA.164_502`) and
2450    /// colon-separated (`transform:mulaw8:pcm16`) catalogue slugs, as
2451    /// well as mixed forms.
2452    ///
2453    /// The grammar is: `segment ((`.` | `:`) segment)*` where a
2454    /// segment is a contiguous run of IDENT / INTEGER tokens (see
2455    /// [`Self::consume_dotted_slug_segment`]).
2456    fn parse_qualifier_value(&mut self) -> Result<String, ParseError> {
2457        let mut buf = self.consume_dotted_slug_segment()?;
2458        loop {
2459            let sep = if self.check(TokenType::Dot) {
2460                '.'
2461            } else if self.check(TokenType::Colon) {
2462                ':'
2463            } else {
2464                break;
2465            };
2466            self.advance();
2467            let part = self.consume_dotted_slug_segment()?;
2468            buf.push(sep);
2469            buf.push_str(&part);
2470        }
2471        Ok(buf)
2472    }
2473
2474    /// Consume a contiguous run of IDENT / INTEGER / keyword-ident
2475    /// tokens whose source positions are adjacent (no whitespace
2476    /// between them), concatenating their text into a single segment.
2477    ///
2478    /// Needed for closed-catalogue qualifier slugs whose segment
2479    /// mixes digits and identifier characters — e.g. `HIPAA.164_502`
2480    /// lexes as INTEGER `164` + IDENT `_502` because `_` starts a
2481    /// fresh identifier; the catalog value is the concatenation
2482    /// `164_502`. Adjacency is determined by matching
2483    /// `(line, column + len)` of the previous token against the next
2484    /// token's start position.
2485    fn consume_dotted_slug_segment(&mut self) -> Result<String, ParseError> {
2486        let first = self.consume_any_ident_or_kw()?;
2487        let mut buf = first.value.clone();
2488        let mut next_line = first.line;
2489        let mut next_col = first.column + first.value.chars().count() as u32;
2490        loop {
2491            let cur = self.current();
2492            let is_segment_token = matches!(cur.ttype, TokenType::Identifier | TokenType::Integer,);
2493            if !is_segment_token {
2494                break;
2495            }
2496            if cur.line != next_line || cur.column != next_col {
2497                break;
2498            }
2499            buf.push_str(&cur.value);
2500            next_col = cur.column + cur.value.chars().count() as u32;
2501            next_line = cur.line;
2502            self.pos += 1;
2503        }
2504        Ok(buf)
2505    }
2506
2507    // ── TYPE ─────────────────────────────────────────────────────
2508
2509    fn parse_type_def(&mut self) -> Result<TypeDefinition, ParseError> {
2510        let tok = self.consume(TokenType::Type)?;
2511        let loc = self.loc_of(&tok);
2512        let name = self.consume(TokenType::Identifier)?.value;
2513
2514        let mut node = TypeDefinition {
2515            name,
2516            fields: Vec::new(),
2517            range_constraint: None,
2518            where_clause: None,
2519            compliance: Vec::new(),
2520            loc: loc.clone(),
2521            leading_trivia: Vec::new(),
2522            trailing_trivia: Vec::new(),
2523        };
2524
2525        // Optional range: (0.0..1.0)
2526        if self.check(TokenType::LParen) {
2527            self.advance();
2528            let min_val = self.consume_number()?;
2529            self.consume(TokenType::DotDot)?;
2530            let max_val = self.consume_number()?;
2531            self.consume(TokenType::RParen)?;
2532            node.range_constraint = Some(RangeConstraint {
2533                min_value: min_val,
2534                max_value: max_val,
2535                loc: loc.clone(),
2536            });
2537        }
2538
2539        // Optional where clause
2540        if self.check(TokenType::Where) {
2541            self.advance();
2542            let mut expr_parts = Vec::new();
2543            while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
2544                if self.check(TokenType::Eof) {
2545                    break;
2546                }
2547                expr_parts.push(self.advance().value.clone());
2548            }
2549            node.where_clause = Some(WhereClause {
2550                expression: expr_parts.join(" "),
2551                loc: loc.clone(),
2552            });
2553        }
2554
2555        // Optional ESK Fase 6.1 — `compliance [HIPAA, ...]` prefix modifier
2556        // between `type Name` / `range` / `where` and the body `{`.
2557        if self.check(TokenType::Identifier) && self.current().value == "compliance" {
2558            self.advance();
2559            node.compliance = self.parse_bracketed_identifiers()?;
2560        }
2561
2562        // Optional body: { field: Type, ... }
2563        if self.check(TokenType::LBrace) {
2564            self.advance();
2565            while !self.check(TokenType::RBrace) {
2566                let field_name = self.consume(TokenType::Identifier)?;
2567                let field_loc = self.loc_of(&field_name);
2568                self.consume(TokenType::Colon)?;
2569                let type_expr = self.parse_type_expr()?;
2570                node.fields.push(TypeField {
2571                    name: field_name.value,
2572                    type_expr,
2573                    loc: field_loc,
2574                });
2575                if self.check(TokenType::Comma) {
2576                    self.advance();
2577                }
2578            }
2579            self.consume(TokenType::RBrace)?;
2580        }
2581
2582        Ok(node)
2583    }
2584
2585    fn parse_type_expr(&mut self) -> Result<TypeExpr, ParseError> {
2586        let name_tok = self.consume(TokenType::Identifier)?;
2587        let loc = self.loc_of(&name_tok);
2588        let mut generic_param = String::new();
2589        let mut optional = false;
2590
2591        if self.check(TokenType::Lt) {
2592            self.advance();
2593            // §Fase 39.a — recursive: the generic param can itself be a
2594            // nested type expression. `FlowEnvelope<List<TenantRecord>>`
2595            // parses as outer=FlowEnvelope, inner=List<TenantRecord>.
2596            // Pre-39.a the inner had to be a single Identifier; nested
2597            // generics like the canonical FlowEnvelope<T> wrapper
2598            // required this lift. Backwards-compat preserved for
2599            // single-level generics like `Stream<Token>` and
2600            // `List<T>` — the recursion lands once and returns the
2601            // same flat string the v1.x parser produced.
2602            let inner = self.parse_type_expr()?;
2603            generic_param = if inner.generic_param.is_empty() {
2604                inner.name
2605            } else {
2606                format!("{}<{}>", inner.name, inner.generic_param)
2607            };
2608            self.consume(TokenType::Gt)?;
2609        }
2610        // §Fase 51.c.3 — bracket type parameters for the continuous-carrier
2611        // grammar: `SymbolicPtr[Tensor[Float32]]`, `DensityMatrix[1024]`. The
2612        // param is either a nested type expression OR a numeric dimension.
2613        if self.check(TokenType::LBracket) {
2614            self.advance();
2615            if matches!(self.current().ttype, TokenType::Integer | TokenType::Float) {
2616                generic_param = self.advance().value.clone();
2617            } else {
2618                let inner = self.parse_type_expr()?;
2619                generic_param = if inner.generic_param.is_empty() {
2620                    inner.name
2621                } else {
2622                    format!("{}[{}]", inner.name, inner.generic_param)
2623                };
2624            }
2625            self.consume(TokenType::RBracket)?;
2626        }
2627        if self.check(TokenType::Question) {
2628            self.advance();
2629            optional = true;
2630        }
2631
2632        Ok(TypeExpr {
2633            name: name_tok.value,
2634            generic_param,
2635            optional,
2636            loc,
2637        })
2638    }
2639
2640    /// Parse a type expression in a context where the AST stores the
2641    /// shape as a flat string (step / reason / forge / ots-apply
2642    /// productions). Mirrors Python `_parse_output_type_string`.
2643    ///
2644    /// Accepts:
2645    /// - `Identifier`        → `"Identifier"`
2646    /// - `Stream<String>`    → `"Stream<String>"`
2647    /// - `Optional?`         → `"Optional?"`
2648    /// - `Stream<String>?`   → `"Stream<String>?"`
2649    ///
2650    /// **Why this exists** — pre-fix, the step parser called
2651    /// `consume(TokenType::Identifier)?.value` which captured only
2652    /// the head identifier and left `< … >` unconsumed. For
2653    /// `output: Stream<Token>`, this produced `output_type =
2654    /// "Stream"`, and downstream `flow_has_stream_output`'s
2655    /// `starts_with("Stream<") && ends_with('>')` predicate then
2656    /// returned false → `implicit_transport == "json"` → the
2657    /// dynamic-route fallback in `axon-rs` served JSON instead of
2658    /// SSE even when the adopter's source canonically declared the
2659    /// algebraic stream effect. Surfaced 2026-05-12 by adopter
2660    /// `docs/MIGRATION_TO_AXON.md` audit after the v1.23.0 wire-
2661    /// layer didn't honor the declarative effect. Python parser was
2662    /// fixed for the same gap 2026-05-09; this is the Rust cross-
2663    /// stack catch-up.
2664    fn parse_output_type_string(&mut self) -> Result<String, ParseError> {
2665        let expr = self.parse_type_expr()?;
2666        let mut s = expr.name;
2667        if !expr.generic_param.is_empty() {
2668            s.push('<');
2669            s.push_str(&expr.generic_param);
2670            s.push('>');
2671        }
2672        if expr.optional {
2673            s.push('?');
2674        }
2675        Ok(s)
2676    }
2677
2678    // ── FLOW ─────────────────────────────────────────────────────
2679
2680    fn parse_flow(&mut self) -> Result<FlowDefinition, ParseError> {
2681        let tok = self.consume(TokenType::Flow)?;
2682        let loc = self.loc_of(&tok);
2683        let name = self.consume(TokenType::Identifier)?.value;
2684
2685        self.consume(TokenType::LParen)?;
2686        let mut parameters = Vec::new();
2687        if !self.check(TokenType::RParen) {
2688            parameters = self.parse_param_list()?;
2689        }
2690        self.consume(TokenType::RParen)?;
2691
2692        let mut return_type = None;
2693        if self.check(TokenType::Arrow) {
2694            self.advance();
2695            return_type = Some(self.parse_type_expr()?);
2696        }
2697
2698        self.consume(TokenType::LBrace)?;
2699        let mut body = Vec::new();
2700        while !self.check(TokenType::RBrace) {
2701            body.push(self.parse_flow_step()?);
2702        }
2703        self.consume(TokenType::RBrace)?;
2704
2705        Ok(FlowDefinition {
2706            name,
2707            parameters,
2708            return_type,
2709            body,
2710            loc,
2711            leading_trivia: Vec::new(),
2712            trailing_trivia: Vec::new(),
2713        })
2714    }
2715
2716    fn parse_param_list(&mut self) -> Result<Vec<Parameter>, ParseError> {
2717        let mut params = Vec::new();
2718
2719        let name = self.consume(TokenType::Identifier)?;
2720        let ploc = self.loc_of(&name);
2721        self.consume(TokenType::Colon)?;
2722        let type_expr = self.parse_type_expr()?;
2723        params.push(Parameter {
2724            name: name.value,
2725            type_expr,
2726            loc: ploc,
2727        });
2728
2729        while self.check(TokenType::Comma) {
2730            self.advance();
2731            let name = self.consume(TokenType::Identifier)?;
2732            let ploc = self.loc_of(&name);
2733            self.consume(TokenType::Colon)?;
2734            let type_expr = self.parse_type_expr()?;
2735            params.push(Parameter {
2736                name: name.value,
2737                type_expr,
2738                loc: ploc,
2739            });
2740        }
2741        Ok(params)
2742    }
2743
2744    // ── FLOW STEP dispatch ───────────────────────────────────────
2745
2746    fn parse_flow_step(&mut self) -> Result<FlowStep, ParseError> {
2747        let tok = self.current().clone();
2748
2749        match tok.ttype {
2750            TokenType::Step => self.parse_step().map(FlowStep::Step),
2751            TokenType::If => self.parse_if().map(FlowStep::If),
2752            TokenType::For => self.parse_for_in().map(FlowStep::ForIn),
2753            TokenType::Let => self.parse_let().map(FlowStep::Let),
2754            TokenType::Return => self.parse_return().map(FlowStep::Return),
2755            TokenType::Break => self.parse_break().map(FlowStep::Break),
2756            TokenType::Continue => self.parse_continue().map(FlowStep::Continue),
2757            TokenType::Lambda => self.parse_lambda_data_apply().map(FlowStep::LambdaDataApply),
2758
2759            // ── Tier 2 flow steps (typed AST) ─────────────────────
2760            TokenType::Probe => self.parse_flow_step_simple("probe").map(|l| FlowStep::Probe(ProbeStep { target: l.1, loc: l.0 })),
2761            TokenType::Reason => self.parse_flow_step_simple("reason").map(|l| FlowStep::Reason(ReasonStep { strategy: String::new(), target: l.1, loc: l.0 })),
2762            TokenType::Validate => self.parse_flow_step_simple("validate").map(|l| FlowStep::Validate(ValidateStep { target: l.1, rule: String::new(), loc: l.0 })),
2763            TokenType::Refine => self.parse_flow_step_simple("refine").map(|l| FlowStep::Refine(RefineStep { target: l.1, strategy: String::new(), loc: l.0 })),
2764            TokenType::Weave => self.parse_weave_step(),
2765            TokenType::Use => self.parse_use_step(),
2766            TokenType::Remember => self.parse_remember_step(),
2767            TokenType::Recall => self.parse_recall_step(),
2768            TokenType::Par => self.parse_par_block().map(FlowStep::Par),
2769            TokenType::Hibernate => self.parse_hibernate_step(),
2770            TokenType::Deliberate => self.parse_block_step("deliberate").map(|l| FlowStep::Deliberate(DeliberateBlock { loc: l })),
2771            TokenType::Consensus => self.parse_block_step("consensus").map(|l| FlowStep::Consensus(ConsensusBlock { loc: l })),
2772            TokenType::Forge => self.parse_block_step("forge").map(|l| FlowStep::Forge(ForgeBlock { loc: l })),
2773            TokenType::Focus => self.parse_flow_step_simple("focus").map(|l| FlowStep::Focus(FocusStep { expression: l.1, loc: l.0 })),
2774            TokenType::Associate => self.parse_associate_step(),
2775            TokenType::Aggregate => self.parse_aggregate_step(),
2776            TokenType::Explore => self.parse_explore_step(),
2777            TokenType::Ingest => self.parse_ingest_step(),
2778            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 })),
2779            TokenType::Stream => self.parse_block_step("stream").map(|l| FlowStep::Stream(StreamBlock { loc: l })),
2780            TokenType::Navigate => self.parse_navigate_step(),
2781            TokenType::Drill => self.parse_drill_step(),
2782            TokenType::Trail => self.parse_flow_step_simple("trail").map(|l| FlowStep::Trail(TrailStep { navigate_ref: l.1, loc: l.0 })),
2783            TokenType::Corroborate => self.parse_corroborate_step(),
2784            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 })),
2785            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 })),
2786            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 })),
2787            TokenType::Listen => self.parse_listen_step(),
2788            TokenType::Daemon => self.parse_flow_step_simple("daemon").map(|l| FlowStep::DaemonStep(DaemonStepNode { daemon_ref: l.1, loc: l.0 })),
2789            // §λ-L-E Fase 13 — Mobile typed channels (paper §3.1, §3.2, §4.3)
2790            TokenType::Emit => self.parse_emit_step(),
2791            TokenType::Publish => self.parse_publish_step(),
2792            TokenType::Discover => self.parse_discover_step(),
2793            TokenType::Persist => self.parse_persist_step(),
2794            TokenType::Retrieve => self.parse_retrieve_step(),
2795            TokenType::Mutate => self.parse_mutate_step(),
2796            TokenType::Purge => self.parse_store_where_step().map(|(loc, store_name, where_expr)| FlowStep::Purge(PurgeStep { store_name, where_expr, loc })),
2797            TokenType::Transact => self.parse_block_step("transact").map(|l| FlowStep::Transact(TransactBlock { loc: l })),
2798            // §Fase 51.a — the `quant` cognitive block (Hilbert-space projection).
2799            TokenType::Quant => self.parse_quant().map(FlowStep::Quant),
2800            // §Fase 51.d.2 — the `yield` measurement point.
2801            TokenType::Yield => self.parse_yield().map(FlowStep::Yield),
2802            // §Fase 52.c — `run <Flow>(args)` as a flow-step: invoke a declared
2803            // flow from inside a body (a `daemon` listen handler, Q3). Reuses
2804            // the top-level run parser.
2805            TokenType::Run => self.parse_run().map(FlowStep::Run),
2806
2807            _ => {
2808                // §Fase 28.e — append "Did you mean X?" hint when the
2809                // unknown token looks like a typo'd flow-body keyword
2810                // (e.g. `stepp` / `reasn` / `validte`). D3, D11.
2811                let hint = crate::smart_suggest::suggest_for(
2812                    &tok.value,
2813                    crate::smart_suggest::FLOW_BODY_KEYWORD_NAMES,
2814                );
2815                let base = format!(
2816                    "Unexpected token in flow body: '{}' — expected step, if, for, let, return, ...",
2817                    tok.value
2818                );
2819                let message = if hint.is_empty() {
2820                    base
2821                } else {
2822                    format!("{base}. {hint}")
2823                };
2824                Err(ParseError {
2825                    message,
2826                    line: tok.line,
2827                    column: tok.column,
2828                    ..Default::default()
2829                })
2830            }
2831        }
2832    }
2833
2834    // ── STEP ─────────────────────────────────────────────────────
2835
2836    fn parse_step(&mut self) -> Result<StepNode, ParseError> {
2837        let tok = self.consume(TokenType::Step)?;
2838        let loc = self.loc_of(&tok);
2839        let name = self.consume(TokenType::Identifier)?.value;
2840
2841        let mut persona_ref = String::new();
2842        if self.check(TokenType::Use) {
2843            self.advance();
2844            persona_ref = self.consume_any_ident_or_kw()?.value;
2845        }
2846
2847        self.consume(TokenType::LBrace)?;
2848
2849        let mut node = StepNode {
2850            name,
2851            persona_ref,
2852            given: String::new(),
2853            ask: String::new(),
2854            output_type: String::new(),
2855            confidence_floor: None,
2856            navigate_ref: String::new(),
2857            apply_ref: String::new(),
2858            requires_context: None,
2859            loc,
2860        };
2861
2862        while !self.check(TokenType::RBrace) {
2863            let inner = self.current().clone();
2864
2865            match inner.ttype {
2866                TokenType::Given => {
2867                    self.advance();
2868                    self.consume(TokenType::Colon)?;
2869                    node.given = self.parse_expression_string()?;
2870                }
2871                TokenType::Ask => {
2872                    self.advance();
2873                    self.consume(TokenType::Colon)?;
2874                    node.ask = self.consume(TokenType::StringLit)?.value;
2875                }
2876                TokenType::Output => {
2877                    // Mirror of Python `_parse_step` `case "output":`
2878                    // which uses `_parse_output_type_string` — accepts
2879                    // the FULL generic-aware shape `Stream<T>`,
2880                    // `Stream<T>?`, `Identifier?`, NOT just the bare
2881                    // head identifier. Pre-fix the step parser dropped
2882                    // `<T>` and downstream `flow_has_stream_output`'s
2883                    // `starts_with("Stream<") && ends_with('>')` then
2884                    // returned false → `implicit_transport == "json"`
2885                    // → dynamic routes served JSON instead of SSE.
2886                    self.advance();
2887                    self.consume(TokenType::Colon)?;
2888                    node.output_type = self.parse_output_type_string()?;
2889                }
2890                TokenType::Navigate => {
2891                    self.advance();
2892                    self.consume(TokenType::Colon)?;
2893                    node.navigate_ref = self.parse_dotted_identifier()?;
2894                }
2895                TokenType::Identifier if inner.value == "confidence_floor" => {
2896                    self.advance();
2897                    self.consume(TokenType::Colon)?;
2898                    node.confidence_floor = Some(self.consume_number()?);
2899                }
2900                TokenType::Identifier if inner.value == "apply" => {
2901                    self.advance();
2902                    self.consume(TokenType::Colon)?;
2903                    node.apply_ref = self.consume_any_ident_or_kw()?.value;
2904                }
2905                // §Fase 68.b — `requires_context: <tokens>`: the step's declared
2906                // model-capability requirement (the context window the cognition
2907                // needs). A bare positive integer literal; the §68.c resolver maps
2908                // it to a concrete model. Range/ceiling is the type-checker's job
2909                // (§68.b positive-int + §68.f catalog ceiling) — the parser only
2910                // requires an integer token here (a float / non-number is a parse
2911                // error, surfaced at the exact column).
2912                TokenType::Identifier if inner.value == "requires_context" => {
2913                    self.advance();
2914                    self.consume(TokenType::Colon)?;
2915                    let num = self.current().clone();
2916                    let bad = |tok: &crate::tokens::Token| ParseError {
2917                        message: format!(
2918                            "`requires_context:` must be a positive integer token count \
2919                             (got '{}')",
2920                            tok.value
2921                        ),
2922                        line: tok.line,
2923                        column: tok.column,
2924                        ..Default::default()
2925                    };
2926                    if num.ttype != TokenType::Integer {
2927                        return Err(bad(&num));
2928                    }
2929                    let value = num.value.parse::<u32>().map_err(|_| bad(&num))?;
2930                    self.advance();
2931                    node.requires_context = Some(value);
2932                }
2933                // §Fase 54.a — a `use` nested inside a `step { }` body used
2934                // to be skipped structurally (grouped with the sub-constructs
2935                // below), silently degrading the tool dispatch to an
2936                // unconstrained LLM step with NO diagnostic. That fallthrough
2937                // drops the AST node before the type-checker can see it, so the
2938                // resource the tool would provision is never linearly accounted
2939                // for (use_tool soundness). Reject it here, at the parser —
2940                // the only place that still sees the token — and redirect to
2941                // the canonical forms.
2942                TokenType::Use => {
2943                    let tool = self
2944                        .tokens
2945                        .get(self.pos + 1)
2946                        .map(|t| t.value.as_str())
2947                        .filter(|v| !v.is_empty())
2948                        .unwrap_or("<Tool>");
2949                    return Err(ParseError {
2950                        message: format!(
2951                            "`use` is not valid inside a `step {{ }}` body — the tool dispatch \
2952                             would be silently dropped. To invoke a tool, either write the \
2953                             flow-level step `use {tool} on <arg>` (outside this block), or bind \
2954                             it inside this step with `apply: {tool}`. To attach a persona, put \
2955                             it in the step header: `step <name> use <Persona> {{ … }}`."
2956                        ),
2957                        line: inner.line,
2958                        column: inner.column,
2959                        ..Default::default()
2960                    });
2961                }
2962                // Sub-constructs (probe, reason, weave, stream) → skip structurally
2963                TokenType::Probe
2964                | TokenType::Reason
2965                | TokenType::Weave
2966                | TokenType::Stream => {
2967                    self.skip_flow_step_structural()?;
2968                }
2969                _ => {
2970                    return Err(ParseError {
2971                        message: format!(
2972                            "Unexpected token in step body: '{}' — expected given, ask, \
2973                             probe, reason, weave, stream, output, confidence_floor, navigate, \
2974                             apply, requires_context",
2975                            inner.value
2976                        ),
2977                        line: inner.line,
2978                        column: inner.column,
2979                                            ..Default::default()
2980                    });
2981                }
2982            }
2983        }
2984        self.consume(TokenType::RBrace)?;
2985        Ok(node)
2986    }
2987
2988    /// Skip a flow-level sub-construct structurally (consume keyword + args + optional block).
2989    fn skip_flow_step_structural(&mut self) -> Result<(), ParseError> {
2990        // Consume the keyword
2991        self.advance();
2992        // Consume tokens until we hit a { or a closing }, or a known flow step keyword
2993        while !self.check(TokenType::LBrace)
2994            && !self.check(TokenType::RBrace)
2995            && !self.check(TokenType::Eof)
2996        {
2997            // Check if we hit a new step-level keyword (means this was a one-liner)
2998            let tt = &self.current().ttype;
2999            if matches!(
3000                tt,
3001                TokenType::Step
3002                    | TokenType::Given
3003                    | TokenType::Ask
3004                    | TokenType::Output
3005                    | TokenType::Navigate
3006                    | TokenType::Use
3007                    | TokenType::Probe
3008                    | TokenType::Reason
3009                    | TokenType::Weave
3010                    | TokenType::Stream
3011                    | TokenType::If
3012                    | TokenType::For
3013                    | TokenType::Let
3014                    | TokenType::Return
3015            ) {
3016                return Ok(());
3017            }
3018            self.advance();
3019        }
3020        // If block, skip it
3021        if self.check(TokenType::LBrace) {
3022            self.skip_braced_block()?;
3023        }
3024        Ok(())
3025    }
3026
3027    // ── INTENT ───────────────────────────────────────────────────
3028
3029    fn parse_intent(&mut self) -> Result<IntentNode, ParseError> {
3030        let tok = self.consume(TokenType::Intent)?;
3031        let loc = self.loc_of(&tok);
3032        let name = self.consume(TokenType::Identifier)?.value;
3033        self.consume(TokenType::LBrace)?;
3034
3035        let mut node = IntentNode {
3036            name,
3037            given: String::new(),
3038            ask: String::new(),
3039            output_type: None,
3040            confidence_floor: None,
3041            loc,
3042            leading_trivia: Vec::new(),
3043            trailing_trivia: Vec::new(),
3044        };
3045
3046        while !self.check(TokenType::RBrace) {
3047            let field_name = self.current().value.clone();
3048            self.advance();
3049            self.consume(TokenType::Colon)?;
3050
3051            match field_name.as_str() {
3052                "given" => node.given = self.consume(TokenType::Identifier)?.value,
3053                "ask" => node.ask = self.consume(TokenType::StringLit)?.value,
3054                "output" => node.output_type = Some(self.parse_type_expr()?),
3055                "confidence_floor" => node.confidence_floor = Some(self.consume_number()?),
3056                _ => self.skip_value(),
3057            }
3058        }
3059        self.consume(TokenType::RBrace)?;
3060        Ok(node)
3061    }
3062
3063    // ── RUN ──────────────────────────────────────────────────────
3064
3065    fn parse_run(&mut self) -> Result<RunStatement, ParseError> {
3066        let tok = self.consume(TokenType::Run)?;
3067        let loc = self.loc_of(&tok);
3068        let flow_name = self.consume(TokenType::Identifier)?.value;
3069
3070        self.consume(TokenType::LParen)?;
3071        let mut arguments = Vec::new();
3072        if !self.check(TokenType::RParen) {
3073            arguments = self.parse_argument_list()?;
3074        }
3075        self.consume(TokenType::RParen)?;
3076
3077        let mut node = RunStatement {
3078            flow_name,
3079            arguments,
3080            persona: String::new(),
3081            context: String::new(),
3082            anchors: Vec::new(),
3083            on_failure: String::new(),
3084            on_failure_params: Vec::new(),
3085            output_to: String::new(),
3086            effort: String::new(),
3087            loc,
3088            leading_trivia: Vec::new(),
3089            trailing_trivia: Vec::new(),
3090        };
3091
3092        while self.check_run_modifier() {
3093            let mod_tok = self.current().clone();
3094            match mod_tok.ttype {
3095                TokenType::As => {
3096                    self.advance();
3097                    node.persona = self.consume(TokenType::Identifier)?.value;
3098                }
3099                TokenType::Within => {
3100                    self.advance();
3101                    node.context = self.consume(TokenType::Identifier)?.value;
3102                }
3103                TokenType::ConstrainedBy => {
3104                    self.advance();
3105                    node.anchors = self.parse_bracketed_identifiers()?;
3106                }
3107                TokenType::OnFailure => {
3108                    self.advance();
3109                    self.consume(TokenType::Colon)?;
3110                    node.on_failure = self.consume_any_ident_or_kw()?.value;
3111                    // Parse optional params: (key: val, ...)
3112                    if self.check(TokenType::LParen) {
3113                        self.advance();
3114                        while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
3115                            let key = self.consume_any_ident_or_kw()?.value;
3116                            self.consume(TokenType::Colon)?;
3117                            let val = self.consume_any_ident_or_kw()?.value;
3118                            node.on_failure_params.push((key, val));
3119                            if self.check(TokenType::Comma) {
3120                                self.advance();
3121                            }
3122                        }
3123                        if self.check(TokenType::RParen) {
3124                            self.advance();
3125                        }
3126                    }
3127                }
3128                TokenType::OutputTo => {
3129                    self.advance();
3130                    self.consume(TokenType::Colon)?;
3131                    node.output_to = self.consume(TokenType::StringLit)?.value;
3132                }
3133                TokenType::Effort => {
3134                    self.advance();
3135                    self.consume(TokenType::Colon)?;
3136                    node.effort = self.consume_any_ident_or_kw()?.value;
3137                }
3138                _ => break,
3139            }
3140        }
3141
3142        Ok(node)
3143    }
3144
3145    // ── EPISTEMIC BLOCK ──────────────────────────────────────────
3146
3147    fn parse_epistemic_block(&mut self) -> Result<EpistemicBlock, ParseError> {
3148        let tok = self.current().clone();
3149        let mode = match tok.ttype {
3150            TokenType::Know => "know",
3151            TokenType::Believe => "believe",
3152            TokenType::Speculate => "speculate",
3153            TokenType::Doubt => "doubt",
3154            _ => unreachable!(),
3155        };
3156        self.advance();
3157        let loc = self.loc_of(&tok);
3158
3159        self.consume(TokenType::LBrace)?;
3160        let mut body = Vec::new();
3161        while !self.check(TokenType::RBrace) {
3162            body.push(self.parse_declaration()?);
3163        }
3164        self.consume(TokenType::RBrace)?;
3165
3166        Ok(EpistemicBlock {
3167            mode: mode.to_string(),
3168            body,
3169            loc,
3170            leading_trivia: Vec::new(),
3171            trailing_trivia: Vec::new(),
3172        })
3173    }
3174
3175    // ── IF ────────────────────────────────────────────────────────
3176
3177    // ── §Fase 70.a — the pure expression engine (Pratt parser) ───────────
3178
3179    /// Parse a pure expression (§Fase 70). Precedence-climbing: `or` < `and` <
3180    /// comparison < `+ -` < `* / %` < unary (`- not`) < atom. Total + pure; no
3181    /// side effects. Field/index access + the builtin catalog land in §70.c/d.
3182    fn parse_expr(&mut self) -> Result<Expr, ParseError> {
3183        self.parse_expr_bp(0)
3184    }
3185
3186    fn parse_expr_bp(&mut self, min_bp: u8) -> Result<Expr, ParseError> {
3187        // Prefix: unary `-` (negation) / `not` (boolean). Binds tighter than
3188        // every binary operator (bp 6).
3189        let mut lhs = match self.current().ttype {
3190            TokenType::Minus => {
3191                self.advance();
3192                Expr::Unary(UnOp::Neg, Box::new(self.parse_expr_bp(6)?))
3193            }
3194            TokenType::Not => {
3195                self.advance();
3196                Expr::Unary(UnOp::Not, Box::new(self.parse_expr_bp(6)?))
3197            }
3198            _ => self.parse_postfix()?,
3199        };
3200        // Infix: left-associative (right_bp = left_bp + 1).
3201        while let Some((op, lbp)) = Self::binop_of(self.current().ttype.clone()) {
3202            if lbp < min_bp {
3203                break;
3204            }
3205            self.advance();
3206            let rhs = self.parse_expr_bp(lbp + 1)?;
3207            lhs = Expr::Binary(op, Box::new(lhs), Box::new(rhs));
3208        }
3209        Ok(lhs)
3210    }
3211
3212    /// Map a token to `(BinOp, left binding power)`, or `None` if it is not an
3213    /// infix operator (which stops the climb — e.g. at `->` or `{`).
3214    fn binop_of(t: TokenType) -> Option<(BinOp, u8)> {
3215        Some(match t {
3216            TokenType::Or => (BinOp::Or, 1),
3217            TokenType::And => (BinOp::And, 2),
3218            TokenType::Eq => (BinOp::Eq, 3),
3219            TokenType::Neq => (BinOp::Ne, 3),
3220            TokenType::Lt => (BinOp::Lt, 3),
3221            TokenType::Lte => (BinOp::Le, 3),
3222            TokenType::Gt => (BinOp::Gt, 3),
3223            TokenType::Gte => (BinOp::Ge, 3),
3224            TokenType::Plus => (BinOp::Add, 4),
3225            TokenType::Minus => (BinOp::Sub, 4),
3226            TokenType::Star => (BinOp::Mul, 5),
3227            TokenType::Slash => (BinOp::Div, 5),
3228            TokenType::Percent => (BinOp::Mod, 5),
3229            _ => return None,
3230        })
3231    }
3232
3233    /// §Fase 70.c — parse a primary then its `.` postfix chain: a builtin call
3234    /// (`.length`, `.contains(x)`) when the name is in the closed catalog, else
3235    /// a dotted reference-path continuation (`a.b.c` → `Ref("a.b.c")`, the
3236    /// pre-§70.c behaviour). Field access on a non-reference (`(a+b).x`) is
3237    /// reserved for §70.d.
3238    fn parse_postfix(&mut self) -> Result<Expr, ParseError> {
3239        let mut expr = self.parse_expr_atom()?;
3240        loop {
3241            if self.check(TokenType::Dot) {
3242                self.advance();
3243                let name = self.consume_any_ident_or_kw()?.value;
3244                if let Some(builtin) = Builtin::from_name(&name) {
3245                    let mut args = vec![expr];
3246                    if self.check(TokenType::LParen) {
3247                        self.advance();
3248                        while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
3249                            args.push(self.parse_expr_bp(0)?);
3250                            if self.check(TokenType::Comma) {
3251                                self.advance();
3252                            } else {
3253                                break;
3254                            }
3255                        }
3256                        self.consume(TokenType::RParen)?;
3257                    }
3258                    expr = Expr::Call(builtin, args);
3259                } else {
3260                    // §Fase 70.d — a plain dotted path on a Ref extends the Ref
3261                    // (back-compat: `a.b.c` → `Ref("a.b.c")`); on any other base
3262                    // it is a structured field access (the JSONB seam).
3263                    expr = match expr {
3264                        Expr::Ref(p) => Expr::Ref(format!("{p}.{name}")),
3265                        other => Expr::Field(Box::new(other), name),
3266                    };
3267                }
3268            } else if self.check(TokenType::LBracket) {
3269                // §Fase 70.d — index access `base[index]`.
3270                self.advance();
3271                let index = self.parse_expr_bp(0)?;
3272                self.consume(TokenType::RBracket)?;
3273                expr = Expr::Index(Box::new(expr), Box::new(index));
3274            } else {
3275                break;
3276            }
3277        }
3278        Ok(expr)
3279    }
3280
3281    fn parse_expr_atom(&mut self) -> Result<Expr, ParseError> {
3282        let tok = self.current().clone();
3283        match tok.ttype {
3284            TokenType::Integer => {
3285                self.advance();
3286                let lit = tok
3287                    .value
3288                    .parse::<i64>()
3289                    .map(ExprLit::Int)
3290                    .or_else(|_| tok.value.parse::<f64>().map(ExprLit::Float))
3291                    .map_err(|_| ParseError {
3292                        message: format!("invalid integer literal '{}'", tok.value),
3293                        line: tok.line,
3294                        column: tok.column,
3295                        ..Default::default()
3296                    })?;
3297                Ok(Expr::Lit(lit))
3298            }
3299            TokenType::Float => {
3300                self.advance();
3301                let f = tok.value.parse::<f64>().map_err(|_| ParseError {
3302                    message: format!("invalid float literal '{}'", tok.value),
3303                    line: tok.line,
3304                    column: tok.column,
3305                    ..Default::default()
3306                })?;
3307                Ok(Expr::Lit(ExprLit::Float(f)))
3308            }
3309            TokenType::Bool => {
3310                self.advance();
3311                Ok(Expr::Lit(ExprLit::Bool(tok.value == "true")))
3312            }
3313            TokenType::StringLit => {
3314                self.advance();
3315                Ok(Expr::Lit(ExprLit::Str(tok.value)))
3316            }
3317            TokenType::LParen => {
3318                self.advance();
3319                let inner = self.parse_expr_bp(0)?;
3320                self.consume(TokenType::RParen)?;
3321                Ok(inner)
3322            }
3323            _ => {
3324                // Reference: a single identifier (or keyword used as a name).
3325                // The `.` chain (dotted path / builtin call) is handled by the
3326                // postfix layer (§70.c `parse_postfix`).
3327                Ok(Expr::Ref(self.consume_any_ident_or_kw()?.value))
3328            }
3329        }
3330    }
3331
3332    /// §Fase 70.a — render a literal to its legacy surface string (for the
3333    /// back-compat `(condition, op, value)` triple). Only used when an
3334    /// expression fits the legacy shape; numeric round-tripping is exact for
3335    /// ints and faithful-enough for floats (the legacy runtime re-parses it).
3336    fn expr_lit_surface(lit: &ExprLit) -> String {
3337        match lit {
3338            ExprLit::Int(i) => i.to_string(),
3339            ExprLit::Float(f) => f.to_string(),
3340            ExprLit::Bool(b) => b.to_string(),
3341            ExprLit::Str(s) => s.clone(),
3342        }
3343    }
3344
3345    fn expr_leaf_surface(expr: &Expr) -> Option<String> {
3346        match expr {
3347            Expr::Ref(p) => Some(p.clone()),
3348            Expr::Lit(l) => Some(Self::expr_lit_surface(l)),
3349            _ => None,
3350        }
3351    }
3352
3353    /// A legacy "leaf" is a bare reference (truthy check) or a
3354    /// `<ref> <cmp> <ref|literal>` triple — exactly what the pre-§70 `if`
3355    /// grammar could express.
3356    fn expr_legacy_leaf(expr: &Expr) -> Option<(String, String, String)> {
3357        match expr {
3358            Expr::Ref(p) => Some((p.clone(), String::new(), String::new())),
3359            Expr::Binary(op, l, r) => {
3360                let op_s = match op {
3361                    BinOp::Eq => "==",
3362                    BinOp::Ne => "!=",
3363                    BinOp::Lt => "<",
3364                    BinOp::Le => "<=",
3365                    BinOp::Gt => ">",
3366                    BinOp::Ge => ">=",
3367                    _ => return None,
3368                };
3369                let lhs = match &**l {
3370                    Expr::Ref(p) => p.clone(),
3371                    _ => return None,
3372                };
3373                let rhs = Self::expr_leaf_surface(r)?;
3374                Some((lhs, op_s.to_string(), rhs))
3375            }
3376            _ => None,
3377        }
3378    }
3379
3380    /// Flatten an `or`-tree of legacy leaves in left-to-right order. Returns
3381    /// `false` (and leaves `out` unusable) if any node is not a legacy leaf.
3382    fn collect_or_leaves(expr: &Expr, out: &mut Vec<(String, String, String)>) -> bool {
3383        match expr {
3384            Expr::Binary(BinOp::Or, l, r) => {
3385                Self::collect_or_leaves(l, out) && Self::collect_or_leaves(r, out)
3386            }
3387            _ => match Self::expr_legacy_leaf(expr) {
3388                Some(t) => {
3389                    out.push(t);
3390                    true
3391                }
3392                None => false,
3393            },
3394        }
3395    }
3396
3397    /// §Fase 70.a — if the parsed condition fits the legacy
3398    /// `(condition, op, value)` + `or`-chain shape, return the legacy fields so
3399    /// the IR + runtime stay byte-identical to pre-§70 (zero drift). `None` ⇒
3400    /// the condition uses richer forms (`and`, `not`, arithmetic, parentheses,
3401    /// nesting) and must ride the `cond` expression evaluator.
3402    #[allow(clippy::type_complexity)]
3403    fn cond_as_legacy(
3404        expr: &Expr,
3405    ) -> Option<(String, String, String, Vec<(String, String, String)>, String)> {
3406        let mut leaves = Vec::new();
3407        if !Self::collect_or_leaves(expr, &mut leaves) || leaves.is_empty() {
3408            return None;
3409        }
3410        let (c0, o0, v0) = leaves[0].clone();
3411        let rest = leaves[1..].to_vec();
3412        let conjunctor = if rest.is_empty() {
3413            String::new()
3414        } else {
3415            "or".to_string()
3416        };
3417        Some((c0, o0, v0, rest, conjunctor))
3418    }
3419
3420    fn parse_if(&mut self) -> Result<ConditionalNode, ParseError> {
3421        let tok = self.consume(TokenType::If)?;
3422        let loc = self.loc_of(&tok);
3423
3424        // §Fase 70.a — parse the condition as a pure expression, then split:
3425        // a legacy-expressible condition populates the legacy triple fields
3426        // (cond = None → byte-identical IR + eval); a richer condition rides
3427        // the `cond` expression evaluator.
3428        let expr = self.parse_expr()?;
3429        let (condition, comparison_op, comparison_value, conditions, conjunctor, cond) =
3430            match Self::cond_as_legacy(&expr) {
3431                Some((c, o, v, more, conj)) => (c, o, v, more, conj, None),
3432                None => (
3433                    String::new(),
3434                    String::new(),
3435                    String::new(),
3436                    Vec::new(),
3437                    String::new(),
3438                    Some(expr),
3439                ),
3440            };
3441
3442        let mut then_body = Vec::new();
3443        let mut else_body = Vec::new();
3444
3445        // Arrow form or block form
3446        if self.check(TokenType::Arrow) {
3447            self.advance();
3448            then_body.push(self.parse_flow_step()?);
3449        } else if self.check(TokenType::LBrace) {
3450            self.advance();
3451            while !self.check(TokenType::RBrace) {
3452                then_body.push(self.parse_flow_step()?);
3453            }
3454            self.consume(TokenType::RBrace)?;
3455        }
3456
3457        // Else branch
3458        if self.check(TokenType::Else) {
3459            self.advance();
3460            if self.check(TokenType::Arrow) {
3461                self.advance();
3462                else_body.push(self.parse_flow_step()?);
3463            } else if self.check(TokenType::LBrace) {
3464                self.advance();
3465                while !self.check(TokenType::RBrace) {
3466                    else_body.push(self.parse_flow_step()?);
3467                }
3468                self.consume(TokenType::RBrace)?;
3469            }
3470        }
3471
3472        Ok(ConditionalNode {
3473            condition,
3474            comparison_op,
3475            comparison_value,
3476            then_body,
3477            else_body,
3478            conditions,
3479            conjunctor,
3480            cond,
3481            loc,
3482        })
3483    }
3484
3485    // ── FOR IN ───────────────────────────────────────────────────
3486
3487    fn parse_for_in(&mut self) -> Result<ForInStatement, ParseError> {
3488        let tok = self.consume(TokenType::For)?;
3489        let loc = self.loc_of(&tok);
3490        let variable = self.consume(TokenType::Identifier)?.value;
3491        self.consume(TokenType::In)?;
3492        let iterable = self.parse_dotted_identifier()?;
3493
3494        self.consume(TokenType::LBrace)?;
3495        // Fase 19.e — increment loop_depth so `parse_break` /
3496        // `parse_continue` inside the body pass the scope check.
3497        // Decrement on every exit path (Ok / Err) so a parse error
3498        // mid-body does not leave the depth permanently elevated
3499        // for later top-level parsing — `?` would skip the
3500        // decrement otherwise.
3501        self.loop_depth += 1;
3502        let body_result = (|| -> Result<Vec<FlowStep>, ParseError> {
3503            let mut body = Vec::new();
3504            while !self.check(TokenType::RBrace) {
3505                body.push(self.parse_flow_step()?);
3506            }
3507            Ok(body)
3508        })();
3509        self.loop_depth -= 1;
3510        let body = body_result?;
3511        self.consume(TokenType::RBrace)?;
3512
3513        Ok(ForInStatement {
3514            variable,
3515            iterable,
3516            body,
3517            loc,
3518        })
3519    }
3520
3521    /// Fase 19.e — `break` keyword. Compile-time scope check
3522    /// (`loop_depth == 0`) rejects break outside a for-in body.
3523    fn parse_break(&mut self) -> Result<BreakStatement, ParseError> {
3524        let tok = self.consume(TokenType::Break)?;
3525        let loc = self.loc_of(&tok);
3526        if self.loop_depth == 0 {
3527            return Err(ParseError {
3528                message: "'break' outside of a for-in loop body".to_string(),
3529                line: tok.line,
3530                column: tok.column,
3531                            ..Default::default()
3532            });
3533        }
3534        Ok(BreakStatement { loc })
3535    }
3536
3537    /// Fase 19.e — `continue` keyword. Same scope check as
3538    /// `parse_break`.
3539    fn parse_continue(&mut self) -> Result<ContinueStatement, ParseError> {
3540        let tok = self.consume(TokenType::Continue)?;
3541        let loc = self.loc_of(&tok);
3542        if self.loop_depth == 0 {
3543            return Err(ParseError {
3544                message: "'continue' outside of a for-in loop body".to_string(),
3545                line: tok.line,
3546                column: tok.column,
3547                            ..Default::default()
3548            });
3549        }
3550        Ok(ContinueStatement { loc })
3551    }
3552
3553    // ── LET ──────────────────────────────────────────────────────
3554
3555    fn parse_let(&mut self) -> Result<LetStatement, ParseError> {
3556        let tok = self.consume(TokenType::Let)?;
3557        let loc = self.loc_of(&tok);
3558
3559        // Name can be an identifier or a keyword used as binding name
3560        let name = self.consume_any_ident_or_kw()?.value;
3561        // §Fase 51.c.3 — optional type annotation `let x: <TypeExpr> = …`.
3562        let type_annotation = if self.check(TokenType::Colon) {
3563            self.advance();
3564            Some(self.parse_type_expr()?)
3565        } else {
3566            None
3567        };
3568        self.consume(TokenType::Assign)?;
3569        // Fase 17.a — reset side-channel before parsing value; the
3570        // atom / expr helpers tag the kind as they descend.
3571        self.last_let_value_kind = "literal".to_string();
3572        let (value, value_ast) = self.parse_let_value_expr_with_ast()?;
3573
3574        Ok(LetStatement {
3575            identifier: name,
3576            value_expr: value,
3577            value_kind: self.last_let_value_kind.clone(),
3578            type_annotation,
3579            value_ast,
3580            loc,
3581            leading_trivia: Vec::new(),
3582            trailing_trivia: Vec::new(),
3583        })
3584    }
3585
3586    fn parse_let_value_expr(&mut self) -> Result<String, ParseError> {
3587        let atom = self.parse_let_atom()?;
3588
3589        // Arithmetic expression: collect as string
3590        if matches!(
3591            self.current().ttype,
3592            TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
3593        ) {
3594            let mut parts = vec![atom];
3595            while matches!(
3596                self.current().ttype,
3597                TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
3598            ) {
3599                parts.push(self.advance().value.clone());
3600                parts.push(self.parse_let_atom()?);
3601            }
3602            self.last_let_value_kind = "expression".to_string();
3603            return Ok(parts.join(" "));
3604        }
3605        Ok(atom)
3606    }
3607
3608    /// §Fase 70.f — parse a `let`-binding value, additionally producing a
3609    /// structured `value_ast` for the expression case. A list literal keeps the
3610    /// dedicated path; everything else is parsed through the §70 expression
3611    /// engine and classified: a bare literal / reference keeps its pre-§70
3612    /// string form (`value_ast = None`, byte-identical), while a real expression
3613    /// (`price * qty`, `recent.length`) additionally carries a `value_ast` the
3614    /// runtime evaluates for real (pre-§70.f it was treated as an opaque literal
3615    /// string). Used ONLY by `parse_let` — other value positions (list items,
3616    /// remember/stream values) keep the string-only `parse_let_value_expr`.
3617    fn parse_let_value_expr_with_ast(&mut self) -> Result<(String, Option<Expr>), ParseError> {
3618        if self.check(TokenType::LBracket) {
3619            self.last_let_value_kind = "literal".to_string();
3620            return Ok((self.parse_let_list_literal()?, None));
3621        }
3622        let expr = self.parse_expr()?;
3623        Ok(match expr {
3624            Expr::Lit(lit) => {
3625                self.last_let_value_kind = "literal".to_string();
3626                (Self::expr_lit_surface(&lit), None)
3627            }
3628            Expr::Ref(p) => {
3629                self.last_let_value_kind = "reference".to_string();
3630                (p, None)
3631            }
3632            other => {
3633                self.last_let_value_kind = "expression".to_string();
3634                (Self::render_expr(&other), Some(other))
3635            }
3636        })
3637    }
3638
3639    /// §Fase 70.f — a readable surface rendering of an expression for the
3640    /// vestigial `value_expr` string (the runtime uses `value_ast`).
3641    fn render_expr(e: &Expr) -> String {
3642        match e {
3643            Expr::Lit(l) => Self::expr_lit_surface(l),
3644            Expr::Ref(p) => p.clone(),
3645            Expr::Unary(UnOp::Neg, x) => format!("-{}", Self::render_expr(x)),
3646            Expr::Unary(UnOp::Not, x) => format!("not {}", Self::render_expr(x)),
3647            Expr::Binary(op, l, r) => {
3648                let sym = match op {
3649                    BinOp::Add => "+",
3650                    BinOp::Sub => "-",
3651                    BinOp::Mul => "*",
3652                    BinOp::Div => "/",
3653                    BinOp::Mod => "%",
3654                    BinOp::Eq => "==",
3655                    BinOp::Ne => "!=",
3656                    BinOp::Lt => "<",
3657                    BinOp::Le => "<=",
3658                    BinOp::Gt => ">",
3659                    BinOp::Ge => ">=",
3660                    BinOp::And => "and",
3661                    BinOp::Or => "or",
3662                };
3663                format!("({} {sym} {})", Self::render_expr(l), Self::render_expr(r))
3664            }
3665            Expr::Call(b, args) => {
3666                let recv = args.first().map(Self::render_expr).unwrap_or_default();
3667                let rest: Vec<String> = args.iter().skip(1).map(Self::render_expr).collect();
3668                if rest.is_empty() {
3669                    format!("{recv}.{}", b.surface())
3670                } else {
3671                    format!("{recv}.{}({})", b.surface(), rest.join(", "))
3672                }
3673            }
3674            Expr::Field(b, f) => format!("{}.{f}", Self::render_expr(b)),
3675            Expr::Index(b, i) => format!("{}[{}]", Self::render_expr(b), Self::render_expr(i)),
3676        }
3677    }
3678
3679    fn parse_let_atom(&mut self) -> Result<String, ParseError> {
3680        let tok = self.current().clone();
3681
3682        match tok.ttype {
3683            TokenType::StringLit => {
3684                self.last_let_value_kind = "literal".to_string();
3685                self.advance();
3686                Ok(tok.value)
3687            }
3688            TokenType::Integer | TokenType::Float => {
3689                self.last_let_value_kind = "literal".to_string();
3690                self.advance();
3691                Ok(tok.value)
3692            }
3693            TokenType::Bool => {
3694                self.last_let_value_kind = "literal".to_string();
3695                self.advance();
3696                Ok(tok.value)
3697            }
3698            TokenType::Identifier => {
3699                self.last_let_value_kind = "reference".to_string();
3700                self.parse_dotted_identifier()
3701            }
3702            TokenType::LBracket => {
3703                self.last_let_value_kind = "literal".to_string();
3704                self.parse_let_list_literal()
3705            }
3706            _ => {
3707                // Keywords starting a dotted path (pix.document_tree)
3708                if self.pos + 1 < self.tokens.len()
3709                    && self.tokens[self.pos + 1].ttype == TokenType::Dot
3710                {
3711                    self.last_let_value_kind = "reference".to_string();
3712                    return self.parse_dotted_identifier();
3713                }
3714                Err(ParseError {
3715                    message: format!(
3716                        "Expected value expression, found {:?}('{}')",
3717                        tok.ttype, tok.value
3718                    ),
3719                    line: tok.line,
3720                    column: tok.column,
3721                                    ..Default::default()
3722                })
3723            }
3724        }
3725    }
3726
3727    fn parse_let_list_literal(&mut self) -> Result<String, ParseError> {
3728        self.consume(TokenType::LBracket)?;
3729        let mut items = Vec::new();
3730        if !self.check(TokenType::RBracket) {
3731            items.push(self.parse_let_value_expr()?);
3732            while self.check(TokenType::Comma) {
3733                self.advance();
3734                if self.check(TokenType::RBracket) {
3735                    break; // trailing comma
3736                }
3737                items.push(self.parse_let_value_expr()?);
3738            }
3739        }
3740        self.consume(TokenType::RBracket)?;
3741        Ok(format!("[{}]", items.join(", ")))
3742    }
3743
3744    // ── RETURN ───────────────────────────────────────────────────
3745
3746    fn parse_return(&mut self) -> Result<ReturnStatement, ParseError> {
3747        let tok = self.consume(TokenType::Return)?;
3748        let loc = self.loc_of(&tok);
3749        let value = self.parse_let_value_expr()?;
3750        Ok(ReturnStatement {
3751            value_expr: value,
3752            loc,
3753        })
3754    }
3755
3756    // ── TIER 2 FLOW STEP HELPERS ────────────────────────────────────
3757
3758    /// Parse: keyword target (consumes keyword + one identifier/keyword-as-value).
3759    fn parse_flow_step_simple(&mut self, _kw: &str) -> Result<(Loc, String), ParseError> {
3760        let tok = self.current().clone();
3761        self.advance(); // consume keyword
3762        let target = if self.at_declaration_start()
3763            || self.check(TokenType::RBrace)
3764            || self.check(TokenType::Eof)
3765        {
3766            String::new()
3767        } else {
3768            self.consume_any_ident_or_kw()?.value.clone()
3769        };
3770        // Skip optional braced block
3771        if self.check(TokenType::LBrace) {
3772            self.skip_braced_block()?;
3773        }
3774        Ok((
3775            Loc {
3776                line: tok.line,
3777                column: tok.column,
3778            },
3779            target,
3780        ))
3781    }
3782
3783    /// Parse: keyword { ... } — block-level step, skip body structurally.
3784    fn parse_block_step(&mut self, _kw: &str) -> Result<Loc, ParseError> {
3785        let tok = self.current().clone();
3786        self.advance();
3787        // Skip optional arguments before brace
3788        while !self.check(TokenType::LBrace)
3789            && !self.check(TokenType::RBrace)
3790            && !self.check(TokenType::Eof)
3791            && !self.at_declaration_start()
3792        {
3793            self.advance();
3794        }
3795        if self.check(TokenType::LBrace) {
3796            self.skip_braced_block()?;
3797        }
3798        Ok(Loc {
3799            line: tok.line,
3800            column: tok.column,
3801        })
3802    }
3803
3804    /// §Fase 65 — Parse `par { stmt1  stmt2  … }` into CONCURRENT branches.
3805    /// Each top-level flow statement inside the block is one branch (a
3806    /// single-statement body); they execute concurrently at runtime
3807    /// (`flow_dispatcher::parallel::run_branches_concurrently`). Before §65 the
3808    /// `par` body was skipped (`parse_block_step`), so the branches were lost
3809    /// and the handler ran as a stub. Multi-statement branches (grouping
3810    /// several steps into one sequential branch) are a future grammar
3811    /// extension; today the natural `par { step A  step B }` fans A and B out.
3812    fn parse_par_block(&mut self) -> Result<ParBlock, ParseError> {
3813        let tok = self.current().clone();
3814        self.advance(); // consume `par`
3815        self.consume(TokenType::LBrace)?;
3816        let mut branches: Vec<Vec<FlowStep>> = Vec::new();
3817        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3818            branches.push(vec![self.parse_flow_step()?]);
3819        }
3820        self.consume(TokenType::RBrace)?;
3821        Ok(ParBlock {
3822            branches,
3823            loc: Loc {
3824                line: tok.line,
3825                column: tok.column,
3826            },
3827        })
3828    }
3829
3830    /// §Fase 51.a — Parse the `quant` cognitive block surface.
3831    ///
3832    /// Grammar (the attribute header is OPTIONAL):
3833    /// ```text
3834    /// quant { <flow steps> }
3835    /// quant(encoding: amplitude, observable: M, qubits: 10,
3836    ///       depth: 4, bandwidth: 0.5, reupload: 3, backend: quant_sim) { <flow steps> }
3837    /// ```
3838    /// The bare form (the paper's example) leaves every attribute defaulted
3839    /// (`encoding = amplitude`, `effect = quant_sim`). The body is parsed into
3840    /// real nested `FlowStep`s — like `par` branches — so §51.b's Continuous
3841    /// Type Invariant scans actual AST rather than skipped tokens.
3842    fn parse_quant(&mut self) -> Result<QuantBlock, ParseError> {
3843        let tok = self.current().clone();
3844        self.advance(); // consume `quant`
3845
3846        let mut block = QuantBlock {
3847            encoding: None,
3848            observable: None,
3849            qubits: None,
3850            depth: None,
3851            bandwidth: None,
3852            reupload: None,
3853            // D1/D9 default backend: the CPU simulator effect. `qpu_native` is
3854            // opt-in via `backend: qpu_native`.
3855            effect: "quant_sim".to_string(),
3856            body: Vec::new(),
3857            loc: Loc {
3858                line: tok.line,
3859                column: tok.column,
3860            },
3861        };
3862
3863        // ── Optional attribute header: `(key: value, …)` ──
3864        if self.check(TokenType::LParen) {
3865            self.advance();
3866            while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
3867                let key = self.consume_any_ident_or_kw()?.value;
3868                self.consume(TokenType::Colon)?;
3869                match key.as_str() {
3870                    "encoding" => {
3871                        block.encoding = Some(self.consume_any_ident_or_kw()?.value)
3872                    }
3873                    "observable" => {
3874                        block.observable = Some(self.parse_dotted_identifier()?)
3875                    }
3876                    "qubits" => block.qubits = Some(self.consume_number()? as i64),
3877                    "depth" => block.depth = Some(self.consume_number()? as i64),
3878                    "bandwidth" => block.bandwidth = Some(self.consume_number()?),
3879                    // §Fase 69.c — data re-uploading layers.
3880                    "reupload" => block.reupload = Some(self.consume_number()? as i64),
3881                    // `backend:` selects the algebraic-effect tag (D1/D9).
3882                    "backend" => block.effect = self.consume_any_ident_or_kw()?.value,
3883                    other => {
3884                        return Err(ParseError {
3885                            message: format!(
3886                                "Unknown `quant` attribute `{other}` — expected one of \
3887                                 encoding, observable, qubits, depth, bandwidth, reupload, backend"
3888                            ),
3889                            line: self.current().line,
3890                            column: self.current().column,
3891                            ..Default::default()
3892                        });
3893                    }
3894                }
3895                // Optional comma between attributes (order-free, trailing-comma ok).
3896                if self.check(TokenType::Comma) {
3897                    self.advance();
3898                }
3899            }
3900            self.consume(TokenType::RParen)?;
3901        }
3902
3903        // ── Body: real nested flow steps (like `par`) ──
3904        self.consume(TokenType::LBrace)?;
3905        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3906            block.body.push(self.parse_flow_step()?);
3907        }
3908        self.consume(TokenType::RBrace)?;
3909
3910        Ok(block)
3911    }
3912
3913    /// §Fase 51.d.2 — Parse the `yield <expr>` measurement point. Reuses the
3914    /// `let`-value expression grammar (reference / literal / arithmetic) so the
3915    /// yielded value's tokenization intent is preserved in `value_kind`.
3916    fn parse_yield(&mut self) -> Result<YieldStatement, ParseError> {
3917        let tok = self.consume(TokenType::Yield)?;
3918        let loc = self.loc_of(&tok);
3919        self.last_let_value_kind = "literal".to_string();
3920        let value_expr = self.parse_let_value_expr()?;
3921        Ok(YieldStatement {
3922            value_expr,
3923            value_kind: self.last_let_value_kind.clone(),
3924            loc,
3925        })
3926    }
3927
3928    /// Parse: keyword Name on target -> output_type (apply pattern).
3929    fn parse_apply_step(&mut self, _kw: &str) -> Result<(Loc, String, String, String), ParseError> {
3930        let tok = self.current().clone();
3931        self.advance(); // consume keyword
3932        let name = self.consume_any_ident_or_kw()?.value.clone();
3933        let mut target = String::new();
3934        let mut output_type = String::new();
3935        // "on" target
3936        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
3937            let next = self.current().clone();
3938            if next.value == "on" {
3939                self.advance();
3940                target = self.consume_any_ident_or_kw()?.value.clone();
3941            }
3942        }
3943        // -> output_type
3944        if self.check(TokenType::Arrow) {
3945            self.advance();
3946            output_type = self.consume_any_ident_or_kw()?.value.clone();
3947        }
3948        // Skip optional braced block
3949        if self.check(TokenType::LBrace) {
3950            self.skip_braced_block()?;
3951        }
3952        Ok((
3953            Loc {
3954                line: tok.line,
3955                column: tok.column,
3956            },
3957            name,
3958            target,
3959            output_type,
3960        ))
3961    }
3962
3963    fn parse_weave_step(&mut self) -> Result<FlowStep, ParseError> {
3964        let tok = self.current().clone();
3965        self.advance();
3966        let mut node = WeaveStep {
3967            sources: Vec::new(),
3968            target: String::new(),
3969            format_type: String::new(),
3970            priority: Vec::new(),
3971            style: String::new(),
3972            loc: Loc {
3973                line: tok.line,
3974                column: tok.column,
3975            },
3976        };
3977        if self.check(TokenType::LBrace) {
3978            self.advance();
3979            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3980                let f = self.current().value.clone();
3981                self.advance();
3982                if self.check(TokenType::Colon) {
3983                    self.advance();
3984                    match f.as_str() {
3985                        "sources" => node.sources = self.parse_bracketed_identifiers()?,
3986                        "target" => node.target = self.consume_any_ident_or_kw()?.value.clone(),
3987                        "format" => {
3988                            node.format_type = self.consume_any_ident_or_kw()?.value.clone()
3989                        }
3990                        "priority" => node.priority = self.parse_bracketed_identifiers()?,
3991                        "style" => node.style = self.consume_any_ident_or_kw()?.value.clone(),
3992                        _ => self.skip_value(),
3993                    }
3994                }
3995            }
3996            if self.check(TokenType::RBrace) {
3997                self.advance();
3998            }
3999        }
4000        Ok(FlowStep::Weave(node))
4001    }
4002
4003    fn parse_use_step(&mut self) -> Result<FlowStep, ParseError> {
4004        let tok = self.current().clone();
4005        self.advance();
4006        let tool_name = self.consume_any_ident_or_kw()?.value.clone();
4007        // §Fase 58.b — two mutually-exclusive `use` argument surfaces:
4008        //   * `use Tool(query = "${q}", max_results = 5)` — D2 canonical
4009        //     multi-field keyword args (§58.b `UseArgs::Named`).
4010        //   * `use Tool on "${arg}"` / `on query` — the §54.b single positional
4011        //     argument (D5 back-compat, `UseArgs::LegacyPositional`):
4012        //       - a STRING LITERAL carrying interpolation (`on "${query}"`)
4013        //         resolved at dispatch against request-bound flow params;
4014        //       - a BARE identifier / literal (`on query` / `on 42`) verbatim.
4015        //     (Unquoted `${query}` is intentionally NOT a form — interpolation
4016        //     lives inside string literals everywhere in Axon.)
4017        let args = if self.check(TokenType::LParen) {
4018            UseArgs::Named(self.parse_named_arg_list()?)
4019        } else {
4020            let mut argument = String::new();
4021            if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4022                let next = self.current().clone();
4023                if next.value == "on" {
4024                    self.advance();
4025                    argument = self.consume_any_ident_or_kw()?.value.clone();
4026                }
4027            }
4028            UseArgs::LegacyPositional(argument)
4029        };
4030        if self.check(TokenType::LBrace) {
4031            self.skip_braced_block()?;
4032        }
4033        Ok(FlowStep::UseTool(UseToolStep {
4034            tool_name,
4035            args,
4036            loc: Loc {
4037                line: tok.line,
4038                column: tok.column,
4039            },
4040        }))
4041    }
4042
4043    /// §Fase 58.b — parse `(name = value, …)` keyword args for the canonical
4044    /// `use Tool(...)` multi-field dispatch. Values are captured as expression
4045    /// strings (StringLit / Integer / Float / Bool / dotted identifier / list)
4046    /// via the shared `parse_let_atom`, since the frontend has no structured
4047    /// `Expr`. A trailing comma is tolerated; `()` yields no args.
4048    fn parse_named_arg_list(&mut self) -> Result<Vec<(String, String, String)>, ParseError> {
4049        self.consume(TokenType::LParen)?;
4050        let mut args = Vec::new();
4051        while !self.check(TokenType::RParen) {
4052            // Accept a keyword-as-name (`filter`, `type`, `from`, …) — real
4053            // adopter schemas use such names; the following `=` disambiguates.
4054            let name = self.consume_any_ident_or_kw()?.value;
4055            self.consume(TokenType::Assign)?;
4056            let value = self.parse_let_atom()?;
4057            // §Fase 60 — `parse_let_atom` classified the value (`"literal"` vs
4058            // `"reference"`); carry it so the runtime resolves a bare
4059            // identifier / `Step.output` as a binding lookup, not a literal.
4060            let value_kind = self.last_let_value_kind.clone();
4061            args.push((name, value, value_kind));
4062            if self.check(TokenType::Comma) {
4063                self.advance();
4064            } else {
4065                break;
4066            }
4067        }
4068        self.consume(TokenType::RParen)?;
4069        Ok(args)
4070    }
4071
4072    fn parse_remember_step(&mut self) -> Result<FlowStep, ParseError> {
4073        let tok = self.current().clone();
4074        self.advance();
4075        let expr = self.consume_any_ident_or_kw()?.value.clone();
4076        let mut mem = String::new();
4077        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4078            let next = self.current().clone();
4079            if next.value == "in" || next.ttype == TokenType::In {
4080                self.advance();
4081                mem = self.consume_any_ident_or_kw()?.value.clone();
4082            }
4083        }
4084        Ok(FlowStep::Remember(RememberStep {
4085            expression: expr,
4086            memory_target: mem,
4087            loc: Loc {
4088                line: tok.line,
4089                column: tok.column,
4090            },
4091        }))
4092    }
4093
4094    fn parse_recall_step(&mut self) -> Result<FlowStep, ParseError> {
4095        let tok = self.current().clone();
4096        self.advance();
4097        let query = if self.check(TokenType::StringLit) {
4098            self.consume(TokenType::StringLit)?.value.clone()
4099        } else {
4100            self.consume_any_ident_or_kw()?.value.clone()
4101        };
4102        let mut mem = String::new();
4103        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4104            let next = self.current().clone();
4105            if next.value == "from" || next.ttype == TokenType::From {
4106                self.advance();
4107                mem = self.consume_any_ident_or_kw()?.value.clone();
4108            }
4109        }
4110        Ok(FlowStep::Recall(RecallStep {
4111            query,
4112            memory_source: mem,
4113            loc: Loc {
4114                line: tok.line,
4115                column: tok.column,
4116            },
4117        }))
4118    }
4119
4120    fn parse_hibernate_step(&mut self) -> Result<FlowStep, ParseError> {
4121        let tok = self.current().clone();
4122        self.advance();
4123        let mut event = String::new();
4124        let mut timeout = String::new();
4125        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4126            event = self.consume_any_ident_or_kw()?.value.clone();
4127        }
4128        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4129            let next = self.current().clone();
4130            if next.ttype == TokenType::Duration {
4131                self.advance();
4132                timeout = next.value.clone();
4133            }
4134        }
4135        Ok(FlowStep::Hibernate(HibernateStep {
4136            event_name: event,
4137            timeout,
4138            loc: Loc {
4139                line: tok.line,
4140                column: tok.column,
4141            },
4142        }))
4143    }
4144
4145    fn parse_associate_step(&mut self) -> Result<FlowStep, ParseError> {
4146        let tok = self.current().clone();
4147        self.advance();
4148        let left = self.consume_any_ident_or_kw()?.value.clone();
4149        let mut right = String::new();
4150        let mut using = String::new();
4151        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4152            right = self.consume_any_ident_or_kw()?.value.clone();
4153        }
4154        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4155            let next = self.current().clone();
4156            if next.value == "using" {
4157                self.advance();
4158                using = self.consume_any_ident_or_kw()?.value.clone();
4159            }
4160        }
4161        Ok(FlowStep::Associate(AssociateStep {
4162            left,
4163            right,
4164            using_field: using,
4165            loc: Loc {
4166                line: tok.line,
4167                column: tok.column,
4168            },
4169        }))
4170    }
4171
4172    fn parse_aggregate_step(&mut self) -> Result<FlowStep, ParseError> {
4173        let tok = self.current().clone();
4174        self.advance();
4175        let target = self.consume_any_ident_or_kw()?.value.clone();
4176        let mut group_by = Vec::new();
4177        let mut alias = String::new();
4178        if self.check(TokenType::LBrace) {
4179            self.advance();
4180            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4181                let f = self.current().value.clone();
4182                self.advance();
4183                if self.check(TokenType::Colon) {
4184                    self.advance();
4185                    match f.as_str() {
4186                        "group_by" => group_by = self.parse_bracketed_identifiers()?,
4187                        "alias" | "as" => alias = self.consume_any_ident_or_kw()?.value.clone(),
4188                        _ => self.skip_value(),
4189                    }
4190                }
4191            }
4192            if self.check(TokenType::RBrace) {
4193                self.advance();
4194            }
4195        }
4196        Ok(FlowStep::Aggregate(AggregateStep {
4197            target,
4198            group_by,
4199            alias,
4200            loc: Loc {
4201                line: tok.line,
4202                column: tok.column,
4203            },
4204        }))
4205    }
4206
4207    fn parse_explore_step(&mut self) -> Result<FlowStep, ParseError> {
4208        let tok = self.current().clone();
4209        self.advance();
4210        let target = self.consume_any_ident_or_kw()?.value.clone();
4211        let mut limit = None;
4212        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4213            if self.current().ttype == TokenType::Integer {
4214                limit = self.current().value.parse::<i64>().ok();
4215                self.advance();
4216            }
4217        }
4218        Ok(FlowStep::ExploreStep(ExploreStepNode {
4219            target,
4220            limit,
4221            loc: Loc {
4222                line: tok.line,
4223                column: tok.column,
4224            },
4225        }))
4226    }
4227
4228    fn parse_ingest_step(&mut self) -> Result<FlowStep, ParseError> {
4229        let tok = self.current().clone();
4230        self.advance();
4231        let source = self.consume_any_ident_or_kw()?.value.clone();
4232        let mut target = String::new();
4233        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4234            let next = self.current().clone();
4235            if next.value == "into" || next.ttype == TokenType::Into {
4236                self.advance();
4237                target = self.consume_any_ident_or_kw()?.value.clone();
4238            }
4239        }
4240        if self.check(TokenType::LBrace) {
4241            self.skip_braced_block()?;
4242        }
4243        Ok(FlowStep::Ingest(IngestStep {
4244            source,
4245            target,
4246            loc: Loc {
4247                line: tok.line,
4248                column: tok.column,
4249            },
4250        }))
4251    }
4252
4253    fn parse_navigate_step(&mut self) -> Result<FlowStep, ParseError> {
4254        let tok = self.current().clone();
4255        self.advance();
4256        let pix_name = self.consume_any_ident_or_kw()?.value.clone();
4257        let mut node = NavigateStep {
4258            pix_name,
4259            corpus_name: String::new(),
4260            query_expr: String::new(),
4261            trail_enabled: false,
4262            output_name: String::new(),
4263            seed: String::new(),
4264            budget: None,
4265            where_expr: String::new(),
4266            loc: Loc {
4267                line: tok.line,
4268                column: tok.column,
4269            },
4270        };
4271        if self.check(TokenType::LBrace) {
4272            self.advance();
4273            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4274                let f = self.current().value.clone();
4275                self.advance();
4276                if self.check(TokenType::Colon) {
4277                    self.advance();
4278                    match f.as_str() {
4279                        "corpus" => {
4280                            node.corpus_name = self.consume_any_ident_or_kw()?.value.clone()
4281                        }
4282                        "query" => {
4283                            node.query_expr = self.consume(TokenType::StringLit)?.value.clone()
4284                        }
4285                        "trail" => {
4286                            node.trail_enabled = self.consume_any_ident_or_kw()?.value == "true"
4287                        }
4288                        "output" | "as" => {
4289                            node.output_name = self.consume_any_ident_or_kw()?.value.clone()
4290                        }
4291                        // §Fase 63.B — MDN corpus-graph navigation.
4292                        "from" => node.seed = self.consume_any_ident_or_kw()?.value.clone(),
4293                        "budget" => node.budget = self.parse_optional_int(),
4294                        // §Fase 66 (Q2) — column-scoped navigation: a raw filter
4295                        // expr (mirrors `retrieve … where`) pushed to the SELECT
4296                        // that sources the corpus `documents:`/`relations:` rows,
4297                        // so a `corpus from axonstore` is scoped to a sub-tenant
4298                        // COLUMN (`where: "tenant_id == '${tenant_id}'"`), not just
4299                        // the axon-tenant RLS scope. Resolved by the §37.d filter
4300                        // compiler at runtime (`${name}` → `$N` bind params).
4301                        "where" => {
4302                            node.where_expr = self.consume(TokenType::StringLit)?.value.clone()
4303                        }
4304                        _ => self.skip_value(),
4305                    }
4306                }
4307            }
4308            if self.check(TokenType::RBrace) {
4309                self.advance();
4310            }
4311        }
4312        Ok(FlowStep::Navigate(node))
4313    }
4314
4315    fn parse_drill_step(&mut self) -> Result<FlowStep, ParseError> {
4316        let tok = self.current().clone();
4317        self.advance();
4318        let pix_name = self.consume_any_ident_or_kw()?.value.clone();
4319        let mut node = DrillStep {
4320            pix_name,
4321            subtree_path: String::new(),
4322            query_expr: String::new(),
4323            output_name: String::new(),
4324            loc: Loc {
4325                line: tok.line,
4326                column: tok.column,
4327            },
4328        };
4329        if self.check(TokenType::LBrace) {
4330            self.advance();
4331            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4332                let f = self.current().value.clone();
4333                self.advance();
4334                if self.check(TokenType::Colon) {
4335                    self.advance();
4336                    match f.as_str() {
4337                        "subtree" | "path" => {
4338                            node.subtree_path = self.consume(TokenType::StringLit)?.value.clone()
4339                        }
4340                        "query" => {
4341                            node.query_expr = self.consume(TokenType::StringLit)?.value.clone()
4342                        }
4343                        "output" | "as" => {
4344                            node.output_name = self.consume_any_ident_or_kw()?.value.clone()
4345                        }
4346                        _ => self.skip_value(),
4347                    }
4348                }
4349            }
4350            if self.check(TokenType::RBrace) {
4351                self.advance();
4352            }
4353        }
4354        Ok(FlowStep::Drill(node))
4355    }
4356
4357    fn parse_corroborate_step(&mut self) -> Result<FlowStep, ParseError> {
4358        let tok = self.current().clone();
4359        self.advance();
4360        let nav_ref = self.consume_any_ident_or_kw()?.value.clone();
4361        let mut output = String::new();
4362        if self.check(TokenType::Arrow) {
4363            self.advance();
4364            output = self.consume_any_ident_or_kw()?.value.clone();
4365        }
4366        Ok(FlowStep::Corroborate(CorroborateStep {
4367            navigate_ref: nav_ref,
4368            output_name: output,
4369            loc: Loc {
4370                line: tok.line,
4371                column: tok.column,
4372            },
4373        }))
4374    }
4375
4376    fn parse_listen_step(&mut self) -> Result<FlowStep, ParseError> {
4377        let tok = self.current().clone();
4378        self.advance();
4379        // §λ-L-E Fase 13 D4 — dual-mode listen:
4380        //   • String topic (legacy, deprecated since Fase 13)
4381        //   • Identifier (canonical: declared ChannelDefinition)
4382        let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
4383            (self.consume(TokenType::StringLit)?.value.clone(), false)
4384        } else {
4385            (self.consume_any_ident_or_kw()?.value.clone(), true)
4386        };
4387        let mut alias = String::new();
4388        if !self.at_declaration_start()
4389            && !self.check(TokenType::RBrace)
4390            && !self.check(TokenType::LBrace)
4391        {
4392            let next = self.current().clone();
4393            if next.value == "as" || next.ttype == TokenType::As {
4394                self.advance();
4395                alias = self.consume_any_ident_or_kw()?.value.clone();
4396            }
4397        }
4398        // §Fase 52.a — parse the handler body into real flow-steps (was
4399        // `skip_braced_block`'d, leaving the listener inert). The body runs on
4400        // each event / scheduled tick.
4401        let body = self.parse_listener_body()?;
4402        Ok(FlowStep::Listen(ListenStep {
4403            channel,
4404            channel_is_ref,
4405            event_alias: alias,
4406            body,
4407            loc: Loc {
4408                line: tok.line,
4409                column: tok.column,
4410            },
4411        }))
4412    }
4413
4414    /// §Fase 52.a — parse a `listen … { <flow steps> }` handler body. The body
4415    /// is OPTIONAL (a bodyless `listen channel` returns an empty Vec); when
4416    /// present, each statement is a real [`FlowStep`] (the same grammar as a
4417    /// flow / `quant` / `par` body), executed per trigger by the §52.c runtime.
4418    fn parse_listener_body(&mut self) -> Result<Vec<FlowStep>, ParseError> {
4419        let mut body = Vec::new();
4420        if self.check(TokenType::LBrace) {
4421            self.advance(); // consume `{`
4422            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4423                body.push(self.parse_flow_step()?);
4424            }
4425            self.consume(TokenType::RBrace)?;
4426        }
4427        Ok(body)
4428    }
4429
4430    fn parse_retrieve_step(&mut self) -> Result<FlowStep, ParseError> {
4431        let tok = self.current().clone();
4432        self.advance();
4433        let store = self.consume_any_ident_or_kw()?.value.clone();
4434        let mut where_expr = String::new();
4435        let mut alias = String::new();
4436        let mut order_by = String::new();
4437        let mut limit_expr = String::new();
4438        if self.check(TokenType::LBrace) {
4439            self.advance();
4440            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4441                let f = self.current().value.clone();
4442                self.advance();
4443                if self.check(TokenType::Colon) {
4444                    self.advance();
4445                    match f.as_str() {
4446                        "where" => where_expr = self.consume(TokenType::StringLit)?.value.clone(),
4447                        "as" | "alias" => alias = self.consume_any_ident_or_kw()?.value.clone(),
4448                        // §Fase 67.b — `order_by:` is a string literal
4449                        // (`"col asc, col2 desc"`), same surface as `where:`.
4450                        "order_by" => {
4451                            order_by = self.consume(TokenType::StringLit)?.value.clone()
4452                        }
4453                        // §Fase 67.b — `limit:` is a bare integer literal
4454                        // (`limit: 100`) OR a string carrying a binding
4455                        // (`limit: "${max}"`). Captured raw; the runtime
4456                        // resolves + validates it as a `u32`.
4457                        "limit" => {
4458                            let t = self.current().clone();
4459                            match t.ttype {
4460                                TokenType::Integer | TokenType::StringLit => {
4461                                    limit_expr = t.value.clone();
4462                                    self.advance();
4463                                }
4464                                _ => self.skip_value(),
4465                            }
4466                        }
4467                        _ => self.skip_value(),
4468                    }
4469                }
4470            }
4471            if self.check(TokenType::RBrace) {
4472                self.advance();
4473            }
4474        }
4475        Ok(FlowStep::Retrieve(RetrieveStep {
4476            store_name: store,
4477            where_expr,
4478            alias,
4479            order_by,
4480            limit_expr,
4481            loc: Loc {
4482                line: tok.line,
4483                column: tok.column,
4484            },
4485        }))
4486    }
4487
4488    /// §Fase 35.m — Parse a `purge` step, capturing the optional
4489    /// `{ where: "<expr>" }` filter. (Fase 35.p moved `mutate` to its
4490    /// own `parse_mutate_step`, which also captures SET columns; this
4491    /// helper now serves `purge` alone — a `DELETE` has no SET clause.)
4492    ///
4493    /// Before Fase 35.m these two steps parsed via `parse_flow_step_simple`,
4494    /// which *skipped* the braced block — so a written `where:` clause
4495    /// was silently dropped and every `mutate`/`purge` ran against the
4496    /// whole store, leaving the entire Fase 35.b/c parameterized-filter
4497    /// machinery unreachable for them. This mirror of `parse_retrieve_step`
4498    /// (minus the `as:` alias — a mutate/purge binds no result) closes
4499    /// that gap. Returns `(loc, store_name, where_expr)`.
4500    fn parse_store_where_step(
4501        &mut self,
4502    ) -> Result<(Loc, String, String), ParseError> {
4503        let tok = self.current().clone();
4504        self.advance(); // consume the keyword
4505        let store = if self.at_declaration_start()
4506            || self.check(TokenType::RBrace)
4507            || self.check(TokenType::Eof)
4508        {
4509            String::new()
4510        } else {
4511            self.consume_any_ident_or_kw()?.value.clone()
4512        };
4513        let mut where_expr = String::new();
4514        if self.check(TokenType::LBrace) {
4515            self.advance();
4516            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4517                let field = self.current().value.clone();
4518                self.advance();
4519                if self.check(TokenType::Colon) {
4520                    self.advance();
4521                    match field.as_str() {
4522                        "where" => {
4523                            where_expr =
4524                                self.consume(TokenType::StringLit)?.value.clone()
4525                        }
4526                        _ => self.skip_value(),
4527                    }
4528                }
4529            }
4530            if self.check(TokenType::RBrace) {
4531                self.advance();
4532            }
4533        }
4534        Ok((
4535            Loc {
4536                line: tok.line,
4537                column: tok.column,
4538            },
4539            store,
4540            where_expr,
4541        ))
4542    }
4543
4544    /// §Fase 35.o — Parse a `persist` step, capturing the optional
4545    /// `{ col: value }` field block.
4546    ///
4547    /// Before Fase 35.o `persist` parsed via `parse_flow_step_simple`,
4548    /// which *skipped* the braced block — so a written field block was
4549    /// silently dropped and the runtime fell back to writing every
4550    /// context binding as a row, which fails against any real table
4551    /// (flows always carry more bindings than a table has columns).
4552    /// This captures the declared columns into `PersistStep.fields`;
4553    /// the runtime writes exactly those (interpolated). A `persist`
4554    /// with no block keeps the v1.30.0 user-bindings fallback — fully
4555    /// backward-compatible. Mirror of `parse_retrieve_step`, but the
4556    /// keys are arbitrary column names rather than the fixed
4557    /// `where:` / `as:` filter keys.
4558    ///
4559    /// The optional `into` connector (`persist into <store>`) is
4560    /// accepted and skipped — before Fase 35.o `into` was captured as
4561    /// the store name.
4562    fn parse_persist_step(&mut self) -> Result<FlowStep, ParseError> {
4563        let tok = self.current().clone();
4564        self.advance(); // consume `persist`
4565        // Optional `into` connector — skip it so the store name that
4566        // follows is not mistaken for the target.
4567        if self.current().value == "into" && !self.check(TokenType::LBrace) {
4568            self.advance();
4569        }
4570        let store = if self.at_declaration_start()
4571            || self.check(TokenType::LBrace)
4572            || self.check(TokenType::RBrace)
4573            || self.check(TokenType::Eof)
4574        {
4575            String::new()
4576        } else {
4577            self.consume_any_ident_or_kw()?.value.clone()
4578        };
4579        let mut fields: Vec<(String, String)> = Vec::new();
4580        if self.check(TokenType::LBrace) {
4581            self.advance();
4582            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4583                let col = self.current().value.clone();
4584                self.advance();
4585                if self.check(TokenType::Colon) {
4586                    self.advance();
4587                    let value = if self.check(TokenType::StringLit) {
4588                        self.consume(TokenType::StringLit)?.value.clone()
4589                    } else if self.check(TokenType::RBrace)
4590                        || self.check(TokenType::Eof)
4591                        || self.check(TokenType::Colon)
4592                    {
4593                        String::new()
4594                    } else {
4595                        let v = self.current().clone();
4596                        self.advance();
4597                        v.value.clone()
4598                    };
4599                    fields.push((col, value));
4600                }
4601            }
4602            if self.check(TokenType::RBrace) {
4603                self.advance();
4604            }
4605        }
4606        Ok(FlowStep::Persist(PersistStep {
4607            store_name: store,
4608            fields,
4609            loc: Loc {
4610                line: tok.line,
4611                column: tok.column,
4612            },
4613        }))
4614    }
4615
4616    /// §Fase 35.p — Parse a `mutate` step, capturing both the
4617    /// `{ where: "<expr>" }` filter AND the `{ col: value }` SET
4618    /// assignments.
4619    ///
4620    /// Before Fase 35.p `mutate` parsed via `parse_store_where_step`,
4621    /// which captured only `where:` and *skipped* every other key — so
4622    /// the runtime built the `UPDATE … SET` clause from every flow
4623    /// binding (params + step results + `let`s), which fails against
4624    /// any real table (`column "X" does not exist`). This closes the
4625    /// gap symmetrically to 35.o's `persist` block: every key other
4626    /// than `where:` is a SET column; a `mutate` with no SET column
4627    /// keeps the v1.31.0 user-bindings fallback. `where:` keeps its
4628    /// string-literal grammar (as in `retrieve` / `purge`).
4629    fn parse_mutate_step(&mut self) -> Result<FlowStep, ParseError> {
4630        let tok = self.current().clone();
4631        self.advance(); // consume `mutate`
4632        let store = if self.at_declaration_start()
4633            || self.check(TokenType::LBrace)
4634            || self.check(TokenType::RBrace)
4635            || self.check(TokenType::Eof)
4636        {
4637            String::new()
4638        } else {
4639            self.consume_any_ident_or_kw()?.value.clone()
4640        };
4641        let mut where_expr = String::new();
4642        let mut fields: Vec<(String, String)> = Vec::new();
4643        if self.check(TokenType::LBrace) {
4644            self.advance();
4645            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4646                let key = self.current().value.clone();
4647                self.advance();
4648                if self.check(TokenType::Colon) {
4649                    self.advance();
4650                    if key == "where" {
4651                        where_expr =
4652                            self.consume(TokenType::StringLit)?.value.clone();
4653                    } else {
4654                        let value = if self.check(TokenType::StringLit) {
4655                            self.consume(TokenType::StringLit)?.value.clone()
4656                        } else if self.check(TokenType::RBrace)
4657                            || self.check(TokenType::Eof)
4658                            || self.check(TokenType::Colon)
4659                        {
4660                            String::new()
4661                        } else {
4662                            let v = self.current().clone();
4663                            self.advance();
4664                            v.value.clone()
4665                        };
4666                        fields.push((key, value));
4667                    }
4668                }
4669            }
4670            if self.check(TokenType::RBrace) {
4671                self.advance();
4672            }
4673        }
4674        Ok(FlowStep::Mutate(MutateStep {
4675            store_name: store,
4676            where_expr,
4677            fields,
4678            loc: Loc {
4679                line: tok.line,
4680                column: tok.column,
4681            },
4682        }))
4683    }
4684
4685    // ── TIER 2 DECLARATIONS ────────────────────────────────────────
4686
4687    fn parse_agent(&mut self) -> Result<AgentDefinition, ParseError> {
4688        let tok = self.consume(TokenType::Agent)?;
4689        let name = self.consume(TokenType::Identifier)?.value;
4690        let mut node = AgentDefinition {
4691            name,
4692            goal: String::new(),
4693            tools: Vec::new(),
4694            memory_ref: String::new(),
4695            strategy: String::new(),
4696            on_stuck: String::new(),
4697            shield_ref: String::new(),
4698            max_iterations: None,
4699            max_tokens: None,
4700            max_time: String::new(),
4701            max_cost: None,
4702            loc: Loc {
4703                line: tok.line,
4704                column: tok.column,
4705            },
4706            leading_trivia: Vec::new(),
4707            trailing_trivia: Vec::new(),
4708        };
4709        // Skip optional parameters/return type before brace
4710        while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
4711            self.advance();
4712        }
4713        self.consume(TokenType::LBrace)?;
4714        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4715            let field = self.current().clone();
4716            let field_name = field.value.clone();
4717            self.advance();
4718            if self.check(TokenType::Colon) {
4719                self.advance();
4720                match field_name.as_str() {
4721                    "goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
4722                    "tools" => node.tools = self.parse_bracketed_identifiers()?,
4723                    "memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
4724                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
4725                    "on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
4726                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
4727                    "max_iterations" => node.max_iterations = self.parse_optional_int(),
4728                    "max_tokens" => node.max_tokens = self.parse_optional_int(),
4729                    "max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
4730                    "max_cost" => node.max_cost = self.parse_optional_float(),
4731                    _ => self.skip_value(),
4732                }
4733            } else if self.check(TokenType::LBrace) {
4734                self.skip_braced_block()?;
4735            }
4736        }
4737        self.consume(TokenType::RBrace)?;
4738        Ok(node)
4739    }
4740
4741    /// §Fase 53 — `extension Name { category: effects|scan, members: [ … ] }`.
4742    /// The parser is permissive on field/category VALUES (validated in
4743    /// §53.c by the type-checker — no-shadowing, category-membership);
4744    /// it only enforces the structural grammar here.
4745    fn parse_extension(&mut self) -> Result<ExtensionDefinition, ParseError> {
4746        let tok = self.consume(TokenType::Extension)?;
4747        let name = self.consume(TokenType::Identifier)?.value;
4748        let mut node = ExtensionDefinition {
4749            name,
4750            category: String::new(),
4751            members: Vec::new(),
4752            loc: Loc {
4753                line: tok.line,
4754                column: tok.column,
4755            },
4756            leading_trivia: Vec::new(),
4757            trailing_trivia: Vec::new(),
4758        };
4759        self.consume(TokenType::LBrace)?;
4760        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4761            let field_name = self.current().value.clone();
4762            self.advance();
4763            if self.check(TokenType::Colon) {
4764                self.advance();
4765                match field_name.as_str() {
4766                    "category" => {
4767                        node.category = self.consume_any_ident_or_kw()?.value.clone()
4768                    }
4769                    "members" => node.members = self.parse_extension_members()?,
4770                    _ => self.skip_value(),
4771                }
4772            } else if self.check(TokenType::LBrace) {
4773                self.skip_braced_block()?;
4774            }
4775        }
4776        self.consume(TokenType::RBrace)?;
4777        Ok(node)
4778    }
4779
4780    /// §Fase 53 — parse `[ "name" [ : { semantics: "…", default_confidence: 0.8 } ], … ]`.
4781    /// Each member is a string literal optionally followed by a metadata
4782    /// block. Trailing/interleaved commas are tolerated.
4783    fn parse_extension_members(&mut self) -> Result<Vec<ExtensionMember>, ParseError> {
4784        let mut members = Vec::new();
4785        self.consume(TokenType::LBracket)?;
4786        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
4787            let name_tok = self.consume(TokenType::StringLit)?;
4788            let mut member = ExtensionMember {
4789                name: name_tok.value.clone(),
4790                semantics: None,
4791                default_confidence: None,
4792                loc: Loc {
4793                    line: name_tok.line,
4794                    column: name_tok.column,
4795                },
4796            };
4797            // Optional `: { semantics: "…", default_confidence: 0.8 }`.
4798            if self.check(TokenType::Colon) {
4799                self.advance();
4800                self.consume(TokenType::LBrace)?;
4801                while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4802                    let mkey = self.current().value.clone();
4803                    self.advance();
4804                    if self.check(TokenType::Colon) {
4805                        self.advance();
4806                        match mkey.as_str() {
4807                            "semantics" => {
4808                                member.semantics =
4809                                    Some(self.consume(TokenType::StringLit)?.value.clone())
4810                            }
4811                            "default_confidence" => {
4812                                member.default_confidence = self.parse_optional_float()
4813                            }
4814                            _ => self.skip_value(),
4815                        }
4816                    }
4817                    if self.check(TokenType::Comma) {
4818                        self.advance();
4819                    }
4820                }
4821                self.consume(TokenType::RBrace)?;
4822            }
4823            members.push(member);
4824            if self.check(TokenType::Comma) {
4825                self.advance();
4826            }
4827        }
4828        self.consume(TokenType::RBracket)?;
4829        Ok(members)
4830    }
4831
4832    fn parse_shield(&mut self) -> Result<ShieldDefinition, ParseError> {
4833        let tok = self.consume(TokenType::Shield)?;
4834        let name = self.consume(TokenType::Identifier)?.value;
4835        let mut node = ShieldDefinition {
4836            name,
4837            scan: Vec::new(),
4838            strategy: String::new(),
4839            on_breach: String::new(),
4840            severity: String::new(),
4841            quarantine: String::new(),
4842            max_retries: None,
4843            confidence_threshold: None,
4844            allow_tools: Vec::new(),
4845            deny_tools: Vec::new(),
4846            sandbox: None,
4847            redact: Vec::new(),
4848            log: String::new(),
4849            deflect_message: String::new(),
4850            taint: String::new(),
4851            compliance: Vec::new(),
4852            loc: Loc {
4853                line: tok.line,
4854                column: tok.column,
4855            },
4856            leading_trivia: Vec::new(),
4857            trailing_trivia: Vec::new(),
4858        };
4859        self.consume(TokenType::LBrace)?;
4860        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4861            let field_name = self.current().value.clone();
4862            self.advance();
4863            if self.check(TokenType::Colon) {
4864                self.advance();
4865                match field_name.as_str() {
4866                    "scan" => node.scan = self.parse_bracketed_identifiers()?,
4867                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
4868                    "on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
4869                    "severity" => node.severity = self.consume_any_ident_or_kw()?.value.clone(),
4870                    "quarantine" => {
4871                        node.quarantine = self.consume(TokenType::StringLit)?.value.clone()
4872                    }
4873                    "max_retries" => node.max_retries = self.parse_optional_int(),
4874                    "confidence_threshold" => {
4875                        node.confidence_threshold = self.parse_optional_float()
4876                    }
4877                    "allow_tools" => node.allow_tools = self.parse_bracketed_identifiers()?,
4878                    "deny_tools" => node.deny_tools = self.parse_bracketed_identifiers()?,
4879                    "sandbox" => {
4880                        node.sandbox = Some(self.consume_any_ident_or_kw()?.value == "true")
4881                    }
4882                    "redact" => node.redact = self.parse_bracketed_identifiers()?,
4883                    "log" => node.log = self.consume_any_ident_or_kw()?.value.clone(),
4884                    "deflect_message" => {
4885                        node.deflect_message = self.consume(TokenType::StringLit)?.value.clone()
4886                    }
4887                    "taint" => node.taint = self.consume_any_ident_or_kw()?.value.clone(),
4888                    // ESK Fase 6.1 — covered regulatory classes.
4889                    "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
4890                    _ => self.skip_value(),
4891                }
4892            } else if self.check(TokenType::LBrace) {
4893                self.skip_braced_block()?;
4894            }
4895        }
4896        self.consume(TokenType::RBrace)?;
4897        Ok(node)
4898    }
4899
4900    fn parse_pix(&mut self) -> Result<PixDefinition, ParseError> {
4901        let tok = self.consume(TokenType::Pix)?;
4902        let name = self.consume(TokenType::Identifier)?.value;
4903        let mut node = PixDefinition {
4904            name,
4905            source: String::new(),
4906            depth: None,
4907            branching: None,
4908            model: String::new(),
4909            loc: Loc {
4910                line: tok.line,
4911                column: tok.column,
4912            },
4913            leading_trivia: Vec::new(),
4914            trailing_trivia: Vec::new(),
4915        };
4916        self.consume(TokenType::LBrace)?;
4917        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4918            let field_name = self.current().value.clone();
4919            self.advance();
4920            if self.check(TokenType::Colon) {
4921                self.advance();
4922                match field_name.as_str() {
4923                    "source" => node.source = self.consume(TokenType::StringLit)?.value.clone(),
4924                    "depth" => node.depth = self.parse_optional_int(),
4925                    "branching" => node.branching = self.parse_optional_int(),
4926                    "model" => node.model = self.consume_any_ident_or_kw()?.value.clone(),
4927                    _ => self.skip_value(),
4928                }
4929            } else if self.check(TokenType::LBrace) {
4930                self.skip_braced_block()?;
4931            }
4932        }
4933        self.consume(TokenType::RBrace)?;
4934        Ok(node)
4935    }
4936
4937    /// §Fase 62.0 — `ledger <Name> { source, depth, branching, model }`.
4938    /// The append-only audit chain (formerly the Provenance-Index reading of
4939    /// `pix`). Field grammar mirrors `pix` (same shape) but the SEMANTICS are
4940    /// audit, not navigation: `depth` = chain retention, `branching` = Merkle
4941    /// factor, `model` = hash slug (sha256 / blake3 / sha3).
4942    fn parse_ledger(&mut self) -> Result<LedgerDefinition, ParseError> {
4943        let tok = self.consume(TokenType::Ledger)?;
4944        let name = self.consume(TokenType::Identifier)?.value;
4945        let mut node = LedgerDefinition {
4946            name,
4947            source: String::new(),
4948            depth: None,
4949            branching: None,
4950            model: String::new(),
4951            loc: Loc {
4952                line: tok.line,
4953                column: tok.column,
4954            },
4955            leading_trivia: Vec::new(),
4956            trailing_trivia: Vec::new(),
4957        };
4958        self.consume(TokenType::LBrace)?;
4959        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4960            let field_name = self.current().value.clone();
4961            self.advance();
4962            if self.check(TokenType::Colon) {
4963                self.advance();
4964                match field_name.as_str() {
4965                    "source" => node.source = self.consume(TokenType::StringLit)?.value.clone(),
4966                    "depth" => node.depth = self.parse_optional_int(),
4967                    "branching" => node.branching = self.parse_optional_int(),
4968                    "model" => node.model = self.consume_any_ident_or_kw()?.value.clone(),
4969                    _ => self.skip_value(),
4970                }
4971            } else if self.check(TokenType::LBrace) {
4972                self.skip_braced_block()?;
4973            }
4974        }
4975        self.consume(TokenType::RBrace)?;
4976        Ok(node)
4977    }
4978
4979    fn parse_psyche(&mut self) -> Result<PsycheDefinition, ParseError> {
4980        let tok = self.consume(TokenType::Psyche)?;
4981        let name = self.consume(TokenType::Identifier)?.value;
4982        let mut node = PsycheDefinition {
4983            name,
4984            dimensions: Vec::new(),
4985            manifold_noise: None,
4986            manifold_momentum: None,
4987            safety_constraints: Vec::new(),
4988            quantum_enabled: None,
4989            inference_mode: String::new(),
4990            loc: Loc {
4991                line: tok.line,
4992                column: tok.column,
4993            },
4994            leading_trivia: Vec::new(),
4995            trailing_trivia: Vec::new(),
4996        };
4997        self.consume(TokenType::LBrace)?;
4998        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4999            let field_name = self.current().value.clone();
5000            self.advance();
5001            if self.check(TokenType::Colon) {
5002                self.advance();
5003                match field_name.as_str() {
5004                    "dimensions" => node.dimensions = self.parse_bracketed_identifiers()?,
5005                    "manifold_noise" => node.manifold_noise = self.parse_optional_float(),
5006                    "manifold_momentum" => node.manifold_momentum = self.parse_optional_float(),
5007                    "safety_constraints" => {
5008                        node.safety_constraints = self.parse_bracketed_identifiers()?
5009                    }
5010                    "quantum_enabled" => {
5011                        node.quantum_enabled = Some(self.consume_any_ident_or_kw()?.value == "true")
5012                    }
5013                    "inference_mode" => {
5014                        node.inference_mode = self.consume_any_ident_or_kw()?.value.clone()
5015                    }
5016                    _ => self.skip_value(),
5017                }
5018            } else if self.check(TokenType::LBrace) {
5019                self.skip_braced_block()?;
5020            }
5021        }
5022        self.consume(TokenType::RBrace)?;
5023        Ok(node)
5024    }
5025
5026    fn parse_corpus(&mut self) -> Result<CorpusDefinition, ParseError> {
5027        let tok = self.consume(TokenType::Corpus)?;
5028        let name = self.consume(TokenType::Identifier)?.value;
5029        let mut node = CorpusDefinition {
5030            name,
5031            documents: Vec::new(),
5032            relations: Vec::new(),
5033            adaptive: false,
5034            mcp_server: String::new(),
5035            mcp_resource_uri: String::new(),
5036            store_source: None,
5037            loc: Loc {
5038                line: tok.line,
5039                column: tok.column,
5040            },
5041            leading_trivia: Vec::new(),
5042            trailing_trivia: Vec::new(),
5043        };
5044        // corpus Name from mcp("server", "uri")  — static MCP-bound short form.
5045        // corpus Name from axonstore { documents: S(id,title)  relations: … }  —
5046        // §Fase 64.A dynamic store-sourced MDN graph (falls through to the body).
5047        let mut dynamic = false;
5048        if self.check(TokenType::From) {
5049            self.advance();
5050            if self.check(TokenType::AxonStore) {
5051                self.advance();
5052                dynamic = true;
5053            } else {
5054                self.consume(TokenType::Mcp)?;
5055                self.consume(TokenType::LParen)?;
5056                node.mcp_server = self.consume(TokenType::StringLit)?.value.clone();
5057                self.consume(TokenType::Comma)?;
5058                node.mcp_resource_uri = self.consume(TokenType::StringLit)?.value.clone();
5059                self.consume(TokenType::RParen)?;
5060                return Ok(node);
5061            }
5062        }
5063        self.consume(TokenType::LBrace)?;
5064        // §Fase 64.A — accumulate the store-mapping pieces while the dynamic body
5065        // is parsed; folded into `node.store_source` after the closing brace.
5066        let mut src = CorpusStoreSource {
5067            doc_store: String::new(),
5068            doc_id_col: String::new(),
5069            doc_title_col: String::new(),
5070            edge_store: String::new(),
5071            edge_from_col: String::new(),
5072            edge_to_col: String::new(),
5073            edge_type_col: String::new(),
5074            edge_weight_col: String::new(),
5075            loc: Loc {
5076                line: tok.line,
5077                column: tok.column,
5078            },
5079        };
5080        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5081            let field_name = self.current().value.clone();
5082            self.advance();
5083            if self.check(TokenType::Colon) {
5084                self.advance();
5085                match field_name.as_str() {
5086                    // §Fase 64.A — dynamic: `documents: <DocStore>(id_col, title_col)`.
5087                    "documents" if dynamic => {
5088                        let (store, cols) = self.parse_corpus_store_mapping(2)?;
5089                        src.doc_store = store;
5090                        src.doc_id_col = cols[0].clone();
5091                        src.doc_title_col = cols[1].clone();
5092                    }
5093                    "documents" => node.documents = self.parse_bracketed_identifiers()?,
5094                    // §Fase 64.A — dynamic: `relations: <EdgeStore>(from, to, etype, weight)`.
5095                    "relations" if dynamic => {
5096                        let (store, cols) = self.parse_corpus_store_mapping(4)?;
5097                        src.edge_store = store;
5098                        src.edge_from_col = cols[0].clone();
5099                        src.edge_to_col = cols[1].clone();
5100                        src.edge_type_col = cols[2].clone();
5101                        src.edge_weight_col = cols[3].clone();
5102                    }
5103                    // §Fase 63.A — static typed weighted edges → MDN corpus graph.
5104                    "relations" => node.relations = self.parse_corpus_relations()?,
5105                    // §Fase 63.C — enable the memory endofunctor.
5106                    "adaptive" => node.adaptive = self.consume_any_ident_or_kw()?.value == "true",
5107                    _ => self.skip_value(),
5108                }
5109            } else if self.check(TokenType::LBrace) {
5110                self.skip_braced_block()?;
5111            }
5112        }
5113        self.consume(TokenType::RBrace)?;
5114        if dynamic {
5115            node.store_source = Some(src);
5116        }
5117        Ok(node)
5118    }
5119
5120    /// §Fase 64.A — parse a store-mapping `<StoreName>( col1, col2, … )` of exactly
5121    /// `n` columns. Used by the dynamic store-sourced corpus's `documents:` (2
5122    /// cols: id, title) and `relations:` (4 cols: from, to, etype, weight). The
5123    /// store name is an identifier (a declared `axonstore`); the columns may be
5124    /// keywords (a column could be named `from`/`type`), so they use the
5125    /// keyword-tolerant consumer. The type-checker validates store + columns.
5126    fn parse_corpus_store_mapping(&mut self, n: usize) -> Result<(String, Vec<String>), ParseError> {
5127        let store = self.consume(TokenType::Identifier)?.value.clone();
5128        self.consume(TokenType::LParen)?;
5129        let mut cols = Vec::with_capacity(n);
5130        for i in 0..n {
5131            if i > 0 {
5132                self.consume(TokenType::Comma)?;
5133            }
5134            cols.push(self.consume_any_ident_or_kw()?.value.clone());
5135        }
5136        self.consume(TokenType::RParen)?;
5137        Ok((store, cols))
5138    }
5139
5140    /// §Fase 63.A — parse `relations: [ etype(from, to, weight) … ]`, the typed
5141    /// weighted edges of an MDN corpus graph. Entries are whitespace/newline
5142    /// separated; commas between them are optional. Edge-type validity (closed
5143    /// catalog), document references, and the weight range are checked by the
5144    /// type-checker (`check_corpus`), not here.
5145    fn parse_corpus_relations(&mut self) -> Result<Vec<CorpusRelation>, ParseError> {
5146        let mut out = Vec::new();
5147        self.consume(TokenType::LBracket)?;
5148        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
5149            if self.check(TokenType::Comma) {
5150                self.advance();
5151                continue;
5152            }
5153            let tok = self.current().clone();
5154            let etype = self.consume_any_ident_or_kw()?.value.clone();
5155            self.consume(TokenType::LParen)?;
5156            let from = self.consume_any_ident_or_kw()?.value.clone();
5157            self.consume(TokenType::Comma)?;
5158            let to = self.consume_any_ident_or_kw()?.value.clone();
5159            self.consume(TokenType::Comma)?;
5160            let weight = self.consume_number()?;
5161            self.consume(TokenType::RParen)?;
5162            out.push(CorpusRelation {
5163                etype,
5164                from,
5165                to,
5166                weight,
5167                loc: Loc { line: tok.line, column: tok.column },
5168            });
5169        }
5170        self.consume(TokenType::RBracket)?;
5171        Ok(out)
5172    }
5173
5174    fn parse_dataspace(&mut self) -> Result<DataspaceDefinition, ParseError> {
5175        let tok = self.consume(TokenType::Dataspace)?;
5176        let name = self.consume(TokenType::Identifier)?.value;
5177        let node = DataspaceDefinition {
5178            name,
5179            loc: Loc {
5180                line: tok.line,
5181                column: tok.column,
5182            },
5183            leading_trivia: Vec::new(),
5184            trailing_trivia: Vec::new(),
5185        };
5186        if self.check(TokenType::LBrace) {
5187            self.skip_braced_block()?;
5188        }
5189        Ok(node)
5190    }
5191
5192    fn parse_ots(&mut self) -> Result<OtsDefinition, ParseError> {
5193        let tok = self.consume(TokenType::Ots)?;
5194        let name = self.consume(TokenType::Identifier)?.value;
5195        let mut node = OtsDefinition {
5196            name,
5197            teleology: String::new(),
5198            homotopy_search: String::new(),
5199            loss_function: String::new(),
5200            loc: Loc {
5201                line: tok.line,
5202                column: tok.column,
5203            },
5204            leading_trivia: Vec::new(),
5205            trailing_trivia: Vec::new(),
5206        };
5207        // Skip optional type params <In, Out>
5208        if self.check(TokenType::Lt) {
5209            while !self.check(TokenType::Gt) && !self.check(TokenType::Eof) {
5210                self.advance();
5211            }
5212            if self.check(TokenType::Gt) {
5213                self.advance();
5214            }
5215        }
5216        self.consume(TokenType::LBrace)?;
5217        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5218            let field_name = self.current().value.clone();
5219            self.advance();
5220            if self.check(TokenType::Colon) {
5221                self.advance();
5222                match field_name.as_str() {
5223                    "teleology" => {
5224                        node.teleology = self.consume(TokenType::StringLit)?.value.clone()
5225                    }
5226                    "homotopy_search" => {
5227                        node.homotopy_search = self.consume_any_ident_or_kw()?.value.clone()
5228                    }
5229                    "loss_function" => {
5230                        node.loss_function = self.consume(TokenType::StringLit)?.value.clone()
5231                    }
5232                    _ => self.skip_value(),
5233                }
5234            } else if self.check(TokenType::LBrace) {
5235                self.skip_braced_block()?;
5236            }
5237        }
5238        self.consume(TokenType::RBrace)?;
5239        Ok(node)
5240    }
5241
5242    fn parse_mandate(&mut self) -> Result<MandateDefinition, ParseError> {
5243        let tok = self.consume(TokenType::Mandate)?;
5244        let name = self.consume(TokenType::Identifier)?.value;
5245        let mut node = MandateDefinition {
5246            name,
5247            constraint: String::new(),
5248            kp: None,
5249            ki: None,
5250            kd: None,
5251            tolerance: None,
5252            max_steps: None,
5253            on_violation: String::new(),
5254            loc: Loc {
5255                line: tok.line,
5256                column: tok.column,
5257            },
5258            leading_trivia: Vec::new(),
5259            trailing_trivia: Vec::new(),
5260        };
5261        self.consume(TokenType::LBrace)?;
5262        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5263            let field_name = self.current().value.clone();
5264            self.advance();
5265            if self.check(TokenType::Colon) {
5266                self.advance();
5267                match field_name.as_str() {
5268                    "constraint" => {
5269                        node.constraint = self.consume(TokenType::StringLit)?.value.clone()
5270                    }
5271                    "kp" | "Kp" => node.kp = self.parse_optional_float(),
5272                    "ki" | "Ki" => node.ki = self.parse_optional_float(),
5273                    "kd" | "Kd" => node.kd = self.parse_optional_float(),
5274                    "tolerance" => node.tolerance = self.parse_optional_float(),
5275                    "max_steps" => node.max_steps = self.parse_optional_int(),
5276                    "on_violation" => {
5277                        node.on_violation = self.consume_any_ident_or_kw()?.value.clone()
5278                    }
5279                    _ => self.skip_value(),
5280                }
5281            } else if self.check(TokenType::LBrace) {
5282                self.skip_braced_block()?;
5283            }
5284        }
5285        self.consume(TokenType::RBrace)?;
5286        Ok(node)
5287    }
5288
5289    fn parse_compute(&mut self) -> Result<ComputeDefinition, ParseError> {
5290        let tok = self.consume(TokenType::Compute)?;
5291        let name = self.consume(TokenType::Identifier)?.value;
5292        let mut node = ComputeDefinition {
5293            name,
5294            shield_ref: String::new(),
5295            loc: Loc {
5296                line: tok.line,
5297                column: tok.column,
5298            },
5299            leading_trivia: Vec::new(),
5300            trailing_trivia: Vec::new(),
5301        };
5302        // Skip optional parameters/return type before brace
5303        while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
5304            self.advance();
5305        }
5306        self.consume(TokenType::LBrace)?;
5307        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5308            let field_name = self.current().value.clone();
5309            self.advance();
5310            if self.check(TokenType::Colon) {
5311                self.advance();
5312                match field_name.as_str() {
5313                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
5314                    _ => self.skip_value(),
5315                }
5316            } else if self.check(TokenType::LBrace) {
5317                self.skip_braced_block()?;
5318            }
5319        }
5320        self.consume(TokenType::RBrace)?;
5321        Ok(node)
5322    }
5323
5324    fn parse_daemon(&mut self) -> Result<DaemonDefinition, ParseError> {
5325        let tok = self.consume(TokenType::Daemon)?;
5326        let name = self.consume(TokenType::Identifier)?.value;
5327        let mut node = DaemonDefinition {
5328            name,
5329            goal: String::new(),
5330            tools: Vec::new(),
5331            memory_ref: String::new(),
5332            strategy: String::new(),
5333            on_stuck: String::new(),
5334            shield_ref: String::new(),
5335            max_tokens: None,
5336            max_time: String::new(),
5337            max_cost: None,
5338            listeners: Vec::new(),
5339            requires_capabilities: Vec::new(),
5340            loc: Loc {
5341                line: tok.line,
5342                column: tok.column,
5343            },
5344            leading_trivia: Vec::new(),
5345            trailing_trivia: Vec::new(),
5346        };
5347        // Skip optional parameters/return type before brace
5348        while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
5349            self.advance();
5350        }
5351        self.consume(TokenType::LBrace)?;
5352        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5353            let field = self.current().clone();
5354            let field_name = field.value.clone();
5355            self.advance();
5356            if self.check(TokenType::Colon) {
5357                self.advance();
5358                match field_name.as_str() {
5359                    "goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
5360                    "tools" => node.tools = self.parse_bracketed_identifiers()?,
5361                    "memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
5362                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
5363                    "on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
5364                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
5365                    "max_tokens" => node.max_tokens = self.parse_optional_int(),
5366                    "max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
5367                    "max_cost" => node.max_cost = self.parse_optional_float(),
5368                    // §Fase 52.d — `requires: [cap, …]` capability scope (same
5369                    // closed slug grammar as `axonendpoint requires:`). The
5370                    // enterprise supervisor mints a per-run principal scoped to
5371                    // exactly these (least privilege).
5372                    "requires" => {
5373                        let bracket_tok = self.current().clone();
5374                        let items = self.parse_bracketed_dot_identifiers()?;
5375                        for slug in &items {
5376                            if !is_valid_capability_slug(slug) {
5377                                return Err(ParseError {
5378                                    message: format!(
5379                                        "Invalid capability slug '{slug}' in daemon '{}' \
5380                                         `requires:`. Capability slugs must match \
5381                                         ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
5382                                         lowercase identifiers. Examples: `daemon.run`, \
5383                                         `memory.write`, `flow.execute`.",
5384                                        node.name
5385                                    ),
5386                                    line: bracket_tok.line,
5387                                    column: bracket_tok.column,
5388                                    ..Default::default()
5389                                });
5390                            }
5391                        }
5392                        node.requires_capabilities = items;
5393                    }
5394                    _ => self.skip_value(),
5395                }
5396            } else if field.ttype == TokenType::Listen {
5397                // §λ-L-E Fase 13 D4 — preserve listen blocks for type
5398                // checking.  We backtracked past the `listen` keyword
5399                // by `advance()` above, so reconstruct a synthetic
5400                // listener using the same dual-mode dispatch the flow
5401                // step parser uses (string topic OR typed channel ref).
5402                let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
5403                    (self.consume(TokenType::StringLit)?.value.clone(), false)
5404                } else {
5405                    (self.consume_any_ident_or_kw()?.value.clone(), true)
5406                };
5407                let mut alias = String::new();
5408                if !self.at_declaration_start()
5409                    && !self.check(TokenType::RBrace)
5410                    && !self.check(TokenType::LBrace)
5411                {
5412                    let next = self.current().clone();
5413                    if next.value == "as" || next.ttype == TokenType::As {
5414                        self.advance();
5415                        alias = self.consume_any_ident_or_kw()?.value.clone();
5416                    }
5417                }
5418                let listen_loc = Loc {
5419                    line: field.line,
5420                    column: field.column,
5421                };
5422                // §Fase 52.a — parse the handler body (was skipped). This is
5423                // what makes a `daemon` operational: the body runs per event /
5424                // scheduled tick (e.g. a `listen "cron:…" as tick { run … }`).
5425                let body = self.parse_listener_body()?;
5426                node.listeners.push(ListenStep {
5427                    channel,
5428                    channel_is_ref,
5429                    event_alias: alias,
5430                    body,
5431                    loc: listen_loc,
5432                });
5433            } else if self.check(TokenType::LBrace) {
5434                self.skip_braced_block()?;
5435            }
5436        }
5437        self.consume(TokenType::RBrace)?;
5438        Ok(node)
5439    }
5440
5441    fn parse_axonstore(&mut self) -> Result<AxonStoreDefinition, ParseError> {
5442        let tok = self.consume(TokenType::AxonStore)?;
5443        let name = self.consume(TokenType::Identifier)?.value;
5444        let mut node = AxonStoreDefinition {
5445            name,
5446            backend: String::new(),
5447            connection: String::new(),
5448            confidence_floor: None,
5449            isolation: String::new(),
5450            on_breach: String::new(),
5451            capability: String::new(),
5452            column_schema: None,
5453            loc: Loc {
5454                line: tok.line,
5455                column: tok.column,
5456            },
5457            leading_trivia: Vec::new(),
5458            trailing_trivia: Vec::new(),
5459        };
5460        self.consume(TokenType::LBrace)?;
5461        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5462            let field = self.current().clone();
5463            let field_name = field.value.clone();
5464            // §Fase 38.b (D1) — `schema:` declaration in three closed
5465            // forms: inline column block, manifest reference (string
5466            // literal), or env-var schema namespace (`env:VAR` —
5467            // unquoted or quoted). Parse the form; the §38.d / §38.e
5468            // type-checker consumes the resulting AST.
5469            if field.ttype == TokenType::Schema {
5470                self.advance();
5471                let parsed = self.parse_store_schema_declaration(&node.name, field.line, field.column)?;
5472                node.column_schema = Some(parsed);
5473                continue;
5474            }
5475            self.advance();
5476            if self.check(TokenType::Colon) {
5477                self.advance();
5478                match field_name.as_str() {
5479                    "backend" => node.backend = self.consume_any_ident_or_kw()?.value.clone(),
5480                    "connection" => {
5481                        node.connection = self.consume(TokenType::StringLit)?.value.clone()
5482                    }
5483                    "confidence_floor" => node.confidence_floor = self.parse_optional_float(),
5484                    "isolation" => node.isolation = self.consume_any_ident_or_kw()?.value.clone(),
5485                    "on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
5486                    // §Fase 35.j (D11) — Pillar IV: the capability slug
5487                    // required to access this store. Validated against
5488                    // the closed slug grammar shared with `requires:`.
5489                    "capability" => {
5490                        let slug_tok = self.consume(TokenType::StringLit)?.clone();
5491                        if !is_valid_capability_slug(&slug_tok.value) {
5492                            return Err(ParseError {
5493                                message: format!(
5494                                    "Invalid capability slug '{}' in axonstore '{}' \
5495                                     `capability:`. Capability slugs must match \
5496                                     ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
5497                                     lowercase identifiers starting with a letter. Examples: \
5498                                     `admin`, `tenant.read`, `hipaa.phi.read`.",
5499                                    slug_tok.value, node.name
5500                                ),
5501                                line: slug_tok.line,
5502                                column: slug_tok.column,
5503                                ..Default::default()
5504                            });
5505                        }
5506                        node.capability = slug_tok.value.clone();
5507                    }
5508                    _ => self.skip_value(),
5509                }
5510            } else if self.check(TokenType::LBrace) {
5511                self.skip_braced_block()?;
5512            }
5513        }
5514        self.consume(TokenType::RBrace)?;
5515        Ok(node)
5516    }
5517
5518    /// §Fase 38.b (D1) — parse the three closed forms of an `axonstore`
5519    /// `schema:` declaration:
5520    ///
5521    ///   * form (a) **inline** — `schema { col: Type [constraint…], … }`
5522    ///   * form (b) **manifest reference** — `schema: "qualified.name"`
5523    ///     (string literal that does NOT start with `env:`)
5524    ///   * form (c) **env-var schema namespace** — `schema: env:VAR`
5525    ///     (unquoted) OR `schema: "env:VAR"` (quoted; the literal
5526    ///     starts with `env:`)
5527    ///
5528    /// Called immediately AFTER `schema` is consumed.
5529    fn parse_store_schema_declaration(
5530        &mut self,
5531        store_name: &str,
5532        sch_line: u32,
5533        sch_col: u32,
5534    ) -> Result<crate::store_schema::StoreColumnSchema, ParseError> {
5535        use crate::store_schema::{StoreColumn, StoreColumnSchema, StoreColumnType};
5536
5537        // — Form (a) — inline column block: `schema { ... }`. —
5538        if self.check(TokenType::LBrace) {
5539            self.consume(TokenType::LBrace)?;
5540            let mut columns: Vec<StoreColumn> = Vec::new();
5541            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5542                let col_tok = self.current().clone();
5543                let col_name = self.consume_any_ident_or_kw()?.value.clone();
5544                self.consume(TokenType::Colon)?;
5545                let type_tok = self.consume_any_ident_or_kw()?.clone();
5546                let col_type = StoreColumnType::from_token(&type_tok.value).ok_or_else(|| {
5547                    let names = StoreColumnType::all_canonical_names();
5548                    let suggestion =
5549                        crate::smart_suggest::suggest_for(&type_tok.value, &names);
5550                    let suggest_suffix = if suggestion.is_empty() {
5551                        String::new()
5552                    } else {
5553                        format!(" {suggestion}")
5554                    };
5555                    let known = names.join(", ");
5556                    ParseError {
5557                        message: format!(
5558                            "Unknown column type `{}` for column `{}` in \
5559                             axonstore `{}` `schema:` block. The closed \
5560                             v1.38.0 column-type catalog (Fase 38.b D1) \
5561                             is {{{known}}} (plus common lowercase \
5562                             aliases — `int`/`integer`/`int4` for \
5563                             `Int`, `bool`/`boolean` for `Bool`, etc.).\
5564                             {suggest_suffix}",
5565                            type_tok.value, col_name, store_name
5566                        ),
5567                        line: type_tok.line,
5568                        column: type_tok.column,
5569                        ..Default::default()
5570                    }
5571                })?;
5572
5573                let mut col = StoreColumn {
5574                    name: col_name,
5575                    col_type,
5576                    primary_key: false,
5577                    auto_increment: false,
5578                    not_null: false,
5579                    unique: false,
5580                    default_value: String::new(),
5581                    // §Fase 38.x.d (D1) — `identity` is now a recognized
5582                    // inline keyword (see the constraint loop below).
5583                    // Defaults to false; set to true when the adopter
5584                    // writes `id: BigInt primary_key identity`.
5585                    identity: false,
5586                    line: col_tok.line,
5587                    column: col_tok.column,
5588                };
5589
5590                // Trailing constraints (position-independent), matching
5591                // the Python `_parse_store_column` surface.
5592                while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5593                    if self.current().ttype != TokenType::Identifier {
5594                        // The next column starts with a non-identifier
5595                        // (rare) — stop the constraint scan.
5596                        break;
5597                    }
5598                    let constraint = self.current().value.clone();
5599                    match constraint.as_str() {
5600                        "primary_key" => {
5601                            col.primary_key = true;
5602                            self.advance();
5603                        }
5604                        "auto_increment" => {
5605                            col.auto_increment = true;
5606                            self.advance();
5607                        }
5608                        "not_null" => {
5609                            col.not_null = true;
5610                            self.advance();
5611                        }
5612                        "unique" => {
5613                            col.unique = true;
5614                            self.advance();
5615                        }
5616                        // §Fase 38.x.d (D1) — `identity` marks a column
5617                        // as `GENERATED ALWAYS/BY DEFAULT AS IDENTITY`.
5618                        // Distinct from `auto_increment` (legacy SERIAL
5619                        // via `nextval(...)` default). T803 skips
5620                        // identity columns from the NOT-NULL-omission
5621                        // check because Postgres auto-fills them; the
5622                        // distinction matters because IDENTITY ALWAYS
5623                        // also rejects user-supplied values, where
5624                        // SERIAL accepts them (a future 38.x.e arm in
5625                        // T802 may surface this).
5626                        "identity" => {
5627                            col.identity = true;
5628                            self.advance();
5629                        }
5630                        "default" => {
5631                            self.advance();
5632                            let dv = self.current().clone();
5633                            if matches!(
5634                                dv.ttype,
5635                                TokenType::StringLit
5636                                    | TokenType::Integer
5637                                    | TokenType::Float
5638                            ) {
5639                                col.default_value = dv.value.clone();
5640                                self.advance();
5641                            } else {
5642                                col.default_value =
5643                                    self.consume_any_ident_or_kw()?.value.clone();
5644                            }
5645                        }
5646                        _ => break,
5647                    }
5648                }
5649
5650                columns.push(col);
5651            }
5652            self.consume(TokenType::RBrace)?;
5653            return Ok(StoreColumnSchema::Inline {
5654                columns,
5655                leading_trivia: Vec::new(),
5656                line: sch_line,
5657                column: sch_col,
5658            });
5659        }
5660
5661        // — Forms (b) + (c) require a `:` separator. —
5662        if !self.check(TokenType::Colon) {
5663            let cur = self.current().clone();
5664            return Err(ParseError {
5665                message: format!(
5666                    "axonstore `{store_name}` `schema:` declaration expects \
5667                     `{{ … }}` (inline columns), `: \"manifest.ref\"` \
5668                     (manifest reference), or `: env:VAR` (per-tenant schema \
5669                     namespace). Got `{}` instead.",
5670                    cur.value
5671                ),
5672                line: cur.line,
5673                column: cur.column,
5674                ..Default::default()
5675            });
5676        }
5677        self.consume(TokenType::Colon)?;
5678
5679        // — Form (b) or (c)-quoted — string literal value. —
5680        if self.check(TokenType::StringLit) {
5681            let lit = self.consume(TokenType::StringLit)?.clone();
5682            let value = lit.value.clone();
5683            if let Some(var) = value.strip_prefix("env:") {
5684                let var = var.trim();
5685                if var.is_empty() {
5686                    return Err(ParseError {
5687                        message: format!(
5688                            "axonstore `{store_name}` `schema: \"env:\"` is \
5689                             missing the variable name after the `env:` \
5690                             prefix."
5691                        ),
5692                        line: lit.line,
5693                        column: lit.column,
5694                        ..Default::default()
5695                    });
5696                }
5697                return Ok(StoreColumnSchema::EnvVar {
5698                    var_name: var.to_string(),
5699                    line: sch_line,
5700                    column: sch_col,
5701                });
5702            }
5703            // Plain string → manifest reference.
5704            if value.trim().is_empty() {
5705                return Err(ParseError {
5706                    message: format!(
5707                        "axonstore `{store_name}` `schema:` manifest reference \
5708                         is empty. Expected `\"qualified.name\"` — e.g. \
5709                         `\"public.tenants\"`."
5710                    ),
5711                    line: lit.line,
5712                    column: lit.column,
5713                    ..Default::default()
5714                });
5715            }
5716            return Ok(StoreColumnSchema::ManifestRef {
5717                qualified_name: value,
5718                line: sch_line,
5719                column: sch_col,
5720            });
5721        }
5722
5723        // — Form (c) unquoted — `env:VAR`. The lexer emits `env` as an
5724        //   identifier, then `:`, then the identifier var name. —
5725        let env_tok = self.current().clone();
5726        if env_tok.value == "env" {
5727            self.advance();
5728            if !self.check(TokenType::Colon) {
5729                return Err(ParseError {
5730                    message: format!(
5731                        "axonstore `{store_name}` `schema: env` is missing the \
5732                         `:` separator. Expected `schema: env:VAR`."
5733                    ),
5734                    line: env_tok.line,
5735                    column: env_tok.column,
5736                    ..Default::default()
5737                });
5738            }
5739            self.advance(); // past ':'
5740            let var_tok = self.consume_any_ident_or_kw()?.clone();
5741            if var_tok.value.trim().is_empty() {
5742                return Err(ParseError {
5743                    message: format!(
5744                        "axonstore `{store_name}` `schema: env:` is missing \
5745                         the variable name."
5746                    ),
5747                    line: var_tok.line,
5748                    column: var_tok.column,
5749                    ..Default::default()
5750                });
5751            }
5752            return Ok(StoreColumnSchema::EnvVar {
5753                var_name: var_tok.value.clone(),
5754                line: sch_line,
5755                column: sch_col,
5756            });
5757        }
5758
5759        Err(ParseError {
5760            message: format!(
5761                "axonstore `{store_name}` `schema:` declaration expects \
5762                 `{{ … }}` (inline columns), `\"manifest.ref\"` (manifest \
5763                 reference), or `env:VAR` (per-tenant schema namespace). \
5764                 Got `{}` instead.",
5765                env_tok.value
5766            ),
5767            line: env_tok.line,
5768            column: env_tok.column,
5769            ..Default::default()
5770        })
5771    }
5772
5773    // ── §λ-L-E Fase 1 — Resource primitive ────────────────────────
5774
5775    /// Parse: `resource Name { kind, endpoint, capacity, lifetime, certainty_floor, shield }`.
5776    ///
5777    /// Mirrors `axon.compiler.parser.Parser._parse_resource`. Unknown fields
5778    /// are silently skipped (keeps the grammar forward-compatible).
5779    fn parse_resource(&mut self) -> Result<ResourceDefinition, ParseError> {
5780        let tok = self.consume(TokenType::Resource)?;
5781        let name = self.consume(TokenType::Identifier)?.value;
5782        let mut node = ResourceDefinition {
5783            name,
5784            kind: String::new(),
5785            endpoint: String::new(),
5786            capacity: None,
5787            lifetime: "affine".to_string(),
5788            certainty_floor: None,
5789            shield_ref: String::new(),
5790            loc: Loc {
5791                line: tok.line,
5792                column: tok.column,
5793            },
5794            leading_trivia: Vec::new(),
5795            trailing_trivia: Vec::new(),
5796        };
5797        self.consume(TokenType::LBrace)?;
5798        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5799            let field_tok = self.current().clone();
5800            let field_name = field_tok.value.clone();
5801            self.advance();
5802            if !self.check(TokenType::Colon) {
5803                // Tolerate stray brace or unknown layout.
5804                if self.check(TokenType::LBrace) {
5805                    self.skip_braced_block()?;
5806                }
5807                continue;
5808            }
5809            self.advance(); // past ':'
5810            match field_name.as_str() {
5811                "kind" => node.kind = self.consume_any_ident_or_kw()?.value,
5812                "endpoint" => node.endpoint = self.consume(TokenType::StringLit)?.value,
5813                "capacity" => {
5814                    node.capacity = self.parse_optional_int();
5815                }
5816                "lifetime" => {
5817                    let lt_tok = self.consume_any_ident_or_kw()?;
5818                    let lt = lt_tok.value;
5819                    if !matches!(lt.as_str(), "linear" | "affine" | "persistent") {
5820                        return Err(ParseError {
5821                            message: format!(
5822                                "Invalid lifetime '{lt}' in resource '{}' — \
5823                                 expected linear | affine | persistent",
5824                                node.name
5825                            ),
5826                            line: lt_tok.line,
5827                            column: lt_tok.column,
5828                                                    ..Default::default()
5829                        });
5830                    }
5831                    node.lifetime = lt;
5832                }
5833                "certainty_floor" => {
5834                    node.certainty_floor = self.parse_optional_float();
5835                }
5836                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
5837                _ => self.skip_value(),
5838            }
5839        }
5840        self.consume(TokenType::RBrace)?;
5841        Ok(node)
5842    }
5843
5844    /// Parse: `fabric Name { provider, region, zones, ephemeral, shield }`.
5845    fn parse_fabric(&mut self) -> Result<FabricDefinition, ParseError> {
5846        let tok = self.consume(TokenType::Fabric)?;
5847        let name = self.consume(TokenType::Identifier)?.value;
5848        let mut node = FabricDefinition {
5849            name,
5850            provider: String::new(),
5851            region: String::new(),
5852            zones: None,
5853            ephemeral: None,
5854            shield_ref: String::new(),
5855            loc: Loc {
5856                line: tok.line,
5857                column: tok.column,
5858            },
5859            leading_trivia: Vec::new(),
5860            trailing_trivia: Vec::new(),
5861        };
5862        self.consume(TokenType::LBrace)?;
5863        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5864            let field_name = self.current().value.clone();
5865            self.advance();
5866            if !self.check(TokenType::Colon) {
5867                if self.check(TokenType::LBrace) {
5868                    self.skip_braced_block()?;
5869                }
5870                continue;
5871            }
5872            self.advance(); // past ':'
5873            match field_name.as_str() {
5874                "provider" => node.provider = self.consume_any_ident_or_kw()?.value,
5875                "region" => node.region = self.consume(TokenType::StringLit)?.value,
5876                "zones" => node.zones = self.parse_optional_int(),
5877                "ephemeral" => {
5878                    let b = self.parse_bool()?;
5879                    node.ephemeral = Some(b);
5880                }
5881                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
5882                _ => self.skip_value(),
5883            }
5884        }
5885        self.consume(TokenType::RBrace)?;
5886        Ok(node)
5887    }
5888
5889    /// Parse: `manifest Name { resources, fabric, region, zones, compliance }`.
5890    fn parse_manifest(&mut self) -> Result<ManifestDefinition, ParseError> {
5891        let tok = self.consume(TokenType::Manifest)?;
5892        let name = self.consume(TokenType::Identifier)?.value;
5893        let mut node = ManifestDefinition {
5894            name,
5895            resources: Vec::new(),
5896            fabric_ref: String::new(),
5897            region: String::new(),
5898            zones: None,
5899            compliance: Vec::new(),
5900            loc: Loc {
5901                line: tok.line,
5902                column: tok.column,
5903            },
5904            leading_trivia: Vec::new(),
5905            trailing_trivia: Vec::new(),
5906        };
5907        self.consume(TokenType::LBrace)?;
5908        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5909            let field_name = self.current().value.clone();
5910            self.advance();
5911            if !self.check(TokenType::Colon) {
5912                if self.check(TokenType::LBrace) {
5913                    self.skip_braced_block()?;
5914                }
5915                continue;
5916            }
5917            self.advance();
5918            match field_name.as_str() {
5919                "resources" => node.resources = self.parse_bracketed_identifiers()?,
5920                "fabric" => node.fabric_ref = self.consume_any_ident_or_kw()?.value,
5921                "region" => node.region = self.consume(TokenType::StringLit)?.value,
5922                "zones" => node.zones = self.parse_optional_int(),
5923                "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
5924                _ => self.skip_value(),
5925            }
5926        }
5927        self.consume(TokenType::RBrace)?;
5928        Ok(node)
5929    }
5930
5931    /// Parse: `observe Name from Manifest { sources, quorum, timeout, on_partition, certainty_floor }`.
5932    fn parse_observe(&mut self) -> Result<ObserveDefinition, ParseError> {
5933        let tok = self.consume(TokenType::Observe)?;
5934        let name = self.consume(TokenType::Identifier)?.value;
5935        // `from <Manifest>` — required per Python grammar.
5936        self.consume(TokenType::From)?;
5937        let target = self.consume(TokenType::Identifier)?.value;
5938        let mut node = ObserveDefinition {
5939            name,
5940            target,
5941            sources: Vec::new(),
5942            quorum: None,
5943            timeout: String::new(),
5944            on_partition: "fail".to_string(),
5945            certainty_floor: None,
5946            loc: Loc {
5947                line: tok.line,
5948                column: tok.column,
5949            },
5950            leading_trivia: Vec::new(),
5951            trailing_trivia: Vec::new(),
5952        };
5953        self.consume(TokenType::LBrace)?;
5954        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5955            let field_name = self.current().value.clone();
5956            self.advance();
5957            if !self.check(TokenType::Colon) {
5958                if self.check(TokenType::LBrace) {
5959                    self.skip_braced_block()?;
5960                }
5961                continue;
5962            }
5963            self.advance();
5964            match field_name.as_str() {
5965                "sources" => node.sources = self.parse_bracketed_identifiers()?,
5966                "quorum" => node.quorum = self.parse_optional_int(),
5967                "timeout" => {
5968                    let t = self.current().clone();
5969                    match t.ttype {
5970                        TokenType::Duration | TokenType::StringLit => {
5971                            self.advance();
5972                            node.timeout = t.value;
5973                        }
5974                        _ => node.timeout = self.consume_any_ident_or_kw()?.value,
5975                    }
5976                }
5977                "on_partition" => {
5978                    let p_tok = self.consume_any_ident_or_kw()?;
5979                    let p = p_tok.value;
5980                    if !matches!(p.as_str(), "fail" | "shield_quarantine") {
5981                        return Err(ParseError {
5982                            message: format!(
5983                                "Invalid on_partition '{p}' in observe '{}' — \
5984                                 expected fail | shield_quarantine",
5985                                node.name
5986                            ),
5987                            line: p_tok.line,
5988                            column: p_tok.column,
5989                                                    ..Default::default()
5990                        });
5991                    }
5992                    node.on_partition = p;
5993                }
5994                "certainty_floor" => node.certainty_floor = self.parse_optional_float(),
5995                _ => self.skip_value(),
5996            }
5997        }
5998        self.consume(TokenType::RBrace)?;
5999        Ok(node)
6000    }
6001
6002    // ── §λ-L-E Fase 3 — Control cognitivo ─────────────────────────
6003
6004    /// Parse: `reconcile Name { observe, threshold, tolerance, on_drift, shield, mandate, max_retries }`.
6005    fn parse_reconcile(&mut self) -> Result<ReconcileDefinition, ParseError> {
6006        let tok = self.consume(TokenType::Reconcile)?;
6007        let name = self.consume(TokenType::Identifier)?.value;
6008        let mut node = ReconcileDefinition {
6009            name,
6010            observe_ref: String::new(),
6011            threshold: None,
6012            tolerance: None,
6013            on_drift: "provision".to_string(),
6014            shield_ref: String::new(),
6015            mandate_ref: String::new(),
6016            max_retries: 3,
6017            loc: Loc {
6018                line: tok.line,
6019                column: tok.column,
6020            },
6021            leading_trivia: Vec::new(),
6022            trailing_trivia: Vec::new(),
6023        };
6024        self.consume(TokenType::LBrace)?;
6025        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6026            let field_name = self.current().value.clone();
6027            self.advance();
6028            if !self.check(TokenType::Colon) {
6029                if self.check(TokenType::LBrace) {
6030                    self.skip_braced_block()?;
6031                }
6032                continue;
6033            }
6034            self.advance();
6035            match field_name.as_str() {
6036                "observe" => node.observe_ref = self.consume_any_ident_or_kw()?.value,
6037                "threshold" => node.threshold = self.parse_optional_float(),
6038                "tolerance" => node.tolerance = self.parse_optional_float(),
6039                "on_drift" => {
6040                    let d_tok = self.consume_any_ident_or_kw()?;
6041                    let d = d_tok.value;
6042                    if !matches!(d.as_str(), "provision" | "alert" | "refine") {
6043                        return Err(ParseError {
6044                            message: format!(
6045                                "Invalid on_drift '{d}' in reconcile '{}' — \
6046                                 expected provision | alert | refine",
6047                                node.name
6048                            ),
6049                            line: d_tok.line,
6050                            column: d_tok.column,
6051                                                    ..Default::default()
6052                        });
6053                    }
6054                    node.on_drift = d;
6055                }
6056                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
6057                "mandate" => node.mandate_ref = self.consume_any_ident_or_kw()?.value,
6058                "max_retries" => {
6059                    if let Some(v) = self.parse_optional_int() {
6060                        node.max_retries = v;
6061                    }
6062                }
6063                _ => self.skip_value(),
6064            }
6065        }
6066        self.consume(TokenType::RBrace)?;
6067        Ok(node)
6068    }
6069
6070    /// Parse: `lease Name { resource, duration, acquire, on_expire }`.
6071    fn parse_lease(&mut self) -> Result<LeaseDefinition, ParseError> {
6072        let tok = self.consume(TokenType::Lease)?;
6073        let name = self.consume(TokenType::Identifier)?.value;
6074        let mut node = LeaseDefinition {
6075            name,
6076            resource_ref: String::new(),
6077            duration: String::new(),
6078            acquire: "on_start".to_string(),
6079            on_expire: "anchor_breach".to_string(),
6080            loc: Loc {
6081                line: tok.line,
6082                column: tok.column,
6083            },
6084            leading_trivia: Vec::new(),
6085            trailing_trivia: Vec::new(),
6086        };
6087        self.consume(TokenType::LBrace)?;
6088        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6089            let field_name = self.current().value.clone();
6090            self.advance();
6091            if !self.check(TokenType::Colon) {
6092                if self.check(TokenType::LBrace) {
6093                    self.skip_braced_block()?;
6094                }
6095                continue;
6096            }
6097            self.advance();
6098            match field_name.as_str() {
6099                "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
6100                "duration" => {
6101                    let t = self.current().clone();
6102                    match t.ttype {
6103                        TokenType::Duration | TokenType::StringLit => {
6104                            self.advance();
6105                            node.duration = t.value;
6106                        }
6107                        _ => node.duration = self.consume_any_ident_or_kw()?.value,
6108                    }
6109                }
6110                "acquire" => {
6111                    let a_tok = self.consume_any_ident_or_kw()?;
6112                    let a = a_tok.value;
6113                    if !matches!(a.as_str(), "on_start" | "on_demand") {
6114                        return Err(ParseError {
6115                            message: format!(
6116                                "Invalid acquire '{a}' in lease '{}' — \
6117                                 expected on_start | on_demand",
6118                                node.name
6119                            ),
6120                            line: a_tok.line,
6121                            column: a_tok.column,
6122                                                    ..Default::default()
6123                        });
6124                    }
6125                    node.acquire = a;
6126                }
6127                "on_expire" => {
6128                    let e_tok = self.consume_any_ident_or_kw()?;
6129                    let e = e_tok.value;
6130                    if !matches!(e.as_str(), "anchor_breach" | "release" | "extend") {
6131                        return Err(ParseError {
6132                            message: format!(
6133                                "Invalid on_expire '{e}' in lease '{}' — \
6134                                 expected anchor_breach | release | extend",
6135                                node.name
6136                            ),
6137                            line: e_tok.line,
6138                            column: e_tok.column,
6139                                                    ..Default::default()
6140                        });
6141                    }
6142                    node.on_expire = e;
6143                }
6144                _ => self.skip_value(),
6145            }
6146        }
6147        self.consume(TokenType::RBrace)?;
6148        Ok(node)
6149    }
6150
6151    /// Parse: `ensemble Name { observations, quorum, aggregation, certainty_mode }`.
6152    fn parse_ensemble(&mut self) -> Result<EnsembleDefinition, ParseError> {
6153        let tok = self.consume(TokenType::Ensemble)?;
6154        let name = self.consume(TokenType::Identifier)?.value;
6155        let mut node = EnsembleDefinition {
6156            name,
6157            observations: Vec::new(),
6158            quorum: None,
6159            aggregation: "majority".to_string(),
6160            certainty_mode: "min".to_string(),
6161            loc: Loc {
6162                line: tok.line,
6163                column: tok.column,
6164            },
6165            leading_trivia: Vec::new(),
6166            trailing_trivia: Vec::new(),
6167        };
6168        self.consume(TokenType::LBrace)?;
6169        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6170            let field_name = self.current().value.clone();
6171            self.advance();
6172            if !self.check(TokenType::Colon) {
6173                if self.check(TokenType::LBrace) {
6174                    self.skip_braced_block()?;
6175                }
6176                continue;
6177            }
6178            self.advance();
6179            match field_name.as_str() {
6180                "observations" => node.observations = self.parse_bracketed_identifiers()?,
6181                "quorum" => node.quorum = self.parse_optional_int(),
6182                "aggregation" => {
6183                    let a_tok = self.consume_any_ident_or_kw()?;
6184                    let a = a_tok.value;
6185                    if !matches!(a.as_str(), "majority" | "weighted" | "byzantine") {
6186                        return Err(ParseError {
6187                            message: format!(
6188                                "Invalid aggregation '{a}' in ensemble '{}' — \
6189                                 expected majority | weighted | byzantine",
6190                                node.name
6191                            ),
6192                            line: a_tok.line,
6193                            column: a_tok.column,
6194                                                    ..Default::default()
6195                        });
6196                    }
6197                    node.aggregation = a;
6198                }
6199                "certainty_mode" => {
6200                    let c_tok = self.consume_any_ident_or_kw()?;
6201                    let c = c_tok.value;
6202                    if !matches!(c.as_str(), "min" | "weighted" | "harmonic") {
6203                        return Err(ParseError {
6204                            message: format!(
6205                                "Invalid certainty_mode '{c}' in ensemble '{}' — \
6206                                 expected min | weighted | harmonic",
6207                                node.name
6208                            ),
6209                            line: c_tok.line,
6210                            column: c_tok.column,
6211                                                    ..Default::default()
6212                        });
6213                    }
6214                    node.certainty_mode = c;
6215                }
6216                _ => self.skip_value(),
6217            }
6218        }
6219        self.consume(TokenType::RBrace)?;
6220        Ok(node)
6221    }
6222
6223    // ── §λ-L-E Fase 4 — Topology + π-calculus binary sessions ─────
6224
6225    /// Parse: `session Name { role1: [step, …]  role2: [step, …] }`.
6226    ///
6227    /// The enclosing `parse_session_definition` disambiguates from the session
6228    /// step token `session` (which does not exist) by always entering from the
6229    /// top-level dispatch; the identifier role name is consumed after `{`.
6230    fn parse_session_definition(&mut self) -> Result<SessionDefinition, ParseError> {
6231        let tok = self.consume(TokenType::Session)?;
6232        let name = self.consume(TokenType::Identifier)?.value;
6233        let mut node = SessionDefinition {
6234            name,
6235            roles: Vec::new(),
6236            loc: Loc {
6237                line: tok.line,
6238                column: tok.column,
6239            },
6240            leading_trivia: Vec::new(),
6241            trailing_trivia: Vec::new(),
6242        };
6243        self.consume(TokenType::LBrace)?;
6244        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6245            let role_tok = self.consume_any_ident_or_kw()?;
6246            self.consume(TokenType::Colon)?;
6247            let steps = self.parse_session_steps()?;
6248            node.roles.push(SessionRole {
6249                name: role_tok.value,
6250                steps,
6251                loc: Loc {
6252                    line: role_tok.line,
6253                    column: role_tok.column,
6254                },
6255            });
6256        }
6257        self.consume(TokenType::RBrace)?;
6258        Ok(node)
6259    }
6260
6261    /// §Fase 51.c.2 — Parse a Pauli-sum observable declaration:
6262    /// ```text
6263    /// observable EnergyHamiltonian {
6264    ///     qubits: 2
6265    ///     term: 0.5 * "ZZ"
6266    ///     term: -1.2 * "XI"
6267    /// }
6268    /// ```
6269    /// `term:` is a repeatable key (one `cₖ · Pₖ` per line). The coefficient is
6270    /// a real scalar (optional leading `+`/`-`), then `*`, then a quoted Pauli
6271    /// string. The type-checker (§51.c.2) validates the closed `{I,X,Y,Z}`
6272    /// alphabet + equal lengths; real coefficients ⇒ Hermitian by construction.
6273    fn parse_observable(&mut self) -> Result<ObservableDefinition, ParseError> {
6274        let tok = self.consume(TokenType::Observable)?;
6275        let name = self.consume(TokenType::Identifier)?.value;
6276        let mut node = ObservableDefinition {
6277            name,
6278            qubits: None,
6279            terms: Vec::new(),
6280            loc: Loc {
6281                line: tok.line,
6282                column: tok.column,
6283            },
6284            leading_trivia: Vec::new(),
6285            trailing_trivia: Vec::new(),
6286        };
6287        self.consume(TokenType::LBrace)?;
6288        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6289            let key_tok = self.consume_any_ident_or_kw()?;
6290            self.consume(TokenType::Colon)?;
6291            match key_tok.value.as_str() {
6292                "qubits" => node.qubits = Some(self.consume_number()? as i64),
6293                "term" => {
6294                    let term_loc = Loc {
6295                        line: key_tok.line,
6296                        column: key_tok.column,
6297                    };
6298                    // Optional sign, then magnitude.
6299                    let mut negative = false;
6300                    if self.check(TokenType::Minus) {
6301                        self.advance();
6302                        negative = true;
6303                    } else if self.check(TokenType::Plus) {
6304                        self.advance();
6305                    }
6306                    let mag = self.consume_number()?;
6307                    let coefficient = if negative { -mag } else { mag };
6308                    // `*` separator between coefficient and Pauli string.
6309                    self.consume(TokenType::Star)?;
6310                    let pauli = self.consume(TokenType::StringLit)?.value;
6311                    node.terms.push(PauliTerm {
6312                        coefficient,
6313                        pauli,
6314                        loc: term_loc,
6315                    });
6316                }
6317                _ => self.skip_value(),
6318            }
6319        }
6320        self.consume(TokenType::RBrace)?;
6321        Ok(node)
6322    }
6323
6324    /// §Fase 69.a — Parse:
6325    /// `witness Name { claim: <ref>  against: <baseline>  metric: <metric>
6326    ///                 threshold: <ε>  data: <source> }`.
6327    /// Order-free `key: value` pairs. `claim`/`against`/`metric`/`data` are bare
6328    /// identifiers (a ref or a closed-catalog keyword); `threshold` is a number.
6329    /// Well-formedness (known metric, threshold range, required fields) is the
6330    /// type-checker's job (`axon-E0790`).
6331    fn parse_witness(&mut self) -> Result<WitnessDefinition, ParseError> {
6332        let tok = self.consume(TokenType::Witness)?;
6333        let name = self.consume(TokenType::Identifier)?.value;
6334        let mut node = WitnessDefinition {
6335            name,
6336            claim: String::new(),
6337            baseline: String::new(),
6338            metric: String::new(),
6339            threshold: 0.0,
6340            data: String::new(),
6341            loc: Loc {
6342                line: tok.line,
6343                column: tok.column,
6344            },
6345            leading_trivia: Vec::new(),
6346            trailing_trivia: Vec::new(),
6347        };
6348        self.consume(TokenType::LBrace)?;
6349        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6350            let key_tok = self.consume_any_ident_or_kw()?;
6351            self.consume(TokenType::Colon)?;
6352            match key_tok.value.as_str() {
6353                "claim" => node.claim = self.consume_any_ident_or_kw()?.value,
6354                // `against` is the baseline; `against` is not a reserved keyword,
6355                // so it lexes as an identifier key here.
6356                "against" => node.baseline = self.consume_any_ident_or_kw()?.value,
6357                "metric" => node.metric = self.consume_any_ident_or_kw()?.value,
6358                "threshold" => node.threshold = self.consume_number()?,
6359                "data" => node.data = self.consume_any_ident_or_kw()?.value,
6360                _ => self.skip_value(),
6361            }
6362        }
6363        self.consume(TokenType::RBrace)?;
6364        Ok(node)
6365    }
6366
6367    /// §Fase 41.b — Parse:
6368    /// `socket Name { protocol: SessionRef, backpressure: credit(n),
6369    ///               reconnect: cognitive_state, legal_basis: ... }`.
6370    /// Fields are `key: value` pairs (order-free); only `protocol` is required.
6371    fn parse_socket(&mut self) -> Result<SocketDefinition, ParseError> {
6372        let tok = self.consume(TokenType::Socket)?;
6373        let name = self.consume(TokenType::Identifier)?.value;
6374        let mut node = SocketDefinition {
6375            name,
6376            loc: Loc { line: tok.line, column: tok.column },
6377            ..Default::default()
6378        };
6379        self.consume(TokenType::LBrace)?;
6380        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6381            let key = self.consume_any_ident_or_kw()?.value;
6382            self.consume(TokenType::Colon)?;
6383            match key.as_str() {
6384                "protocol" => node.protocol = self.consume_any_ident_or_kw()?.value,
6385                "backpressure" => {
6386                    // `credit(n)` — the typed-resource window.
6387                    let kind = self.consume_any_ident_or_kw()?.value;
6388                    if kind != "credit" {
6389                        return Err(self.error(&format!("expected `credit(n)` for backpressure, got `{kind}`")));
6390                    }
6391                    self.consume(TokenType::LParen)?;
6392                    let n = self
6393                        .consume(TokenType::Integer)?
6394                        .value
6395                        .parse::<i64>()
6396                        .map_err(|_| self.error("backpressure credit must be an integer"))?;
6397                    self.consume(TokenType::RParen)?;
6398                    node.backpressure_credit = Some(n);
6399                }
6400                "reconnect" => {
6401                    let mode = self.consume_any_ident_or_kw()?.value;
6402                    node.reconnect = mode == "cognitive_state";
6403                }
6404                "legal_basis" => node.legal_basis = Some(self.consume_any_ident_or_kw()?.value),
6405                other => return Err(self.error(&format!("unknown socket field `{other}`"))),
6406            }
6407            // Optional comma between fields.
6408            if self.check(TokenType::Comma) {
6409                self.consume(TokenType::Comma)?;
6410            }
6411        }
6412        self.consume(TokenType::RBrace)?;
6413        Ok(node)
6414    }
6415
6416    /// Parse: `[send T, receive U, loop, end]`.
6417    fn parse_session_steps(&mut self) -> Result<Vec<SessionStep>, ParseError> {
6418        self.consume(TokenType::LBracket)?;
6419        let mut steps = Vec::new();
6420        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
6421            steps.push(self.parse_session_step()?);
6422            if self.check(TokenType::Comma) {
6423                self.advance();
6424            }
6425        }
6426        self.consume(TokenType::RBracket)?;
6427        Ok(steps)
6428    }
6429
6430    fn parse_session_step(&mut self) -> Result<SessionStep, ParseError> {
6431        let tok = self.current().clone();
6432        let loc = Loc { line: tok.line, column: tok.column };
6433        match tok.ttype {
6434            TokenType::Send => {
6435                self.advance();
6436                let msg = self.consume_any_ident_or_kw()?;
6437                Ok(SessionStep { op: "send".into(), message_type: msg.value, loc, ..Default::default() })
6438            }
6439            TokenType::Receive => {
6440                self.advance();
6441                let msg = self.consume_any_ident_or_kw()?;
6442                Ok(SessionStep { op: "receive".into(), message_type: msg.value, loc, ..Default::default() })
6443            }
6444            TokenType::Loop => {
6445                self.advance();
6446                Ok(SessionStep { op: "loop".into(), loc, ..Default::default() })
6447            }
6448            TokenType::End => {
6449                self.advance();
6450                Ok(SessionStep { op: "end".into(), loc, ..Default::default() })
6451            }
6452            // §Fase 41.b — choice: `select { ℓ: [..], … }` (⊕) | `branch { ℓ: [..], … }` (&).
6453            // `select`/`branch` are not keywords — they arrive as identifiers.
6454            TokenType::Identifier if tok.value == "select" || tok.value == "branch" => {
6455                self.parse_session_choice(&tok.value, loc)
6456            }
6457            _ => Err(ParseError {
6458                message: format!(
6459                    "Invalid session step '{}' — expected send | receive | loop | end | select | branch",
6460                    tok.value
6461                ),
6462                line: tok.line,
6463                column: tok.column,
6464                ..Default::default()
6465            }),
6466        }
6467    }
6468
6469    /// §Fase 41.b — Parse a choice step: `select { ask: [..], cancel: [..] }`
6470    /// (or `branch { … }`). Each `label: [steps]` arm is a nested sub-protocol.
6471    fn parse_session_choice(&mut self, op: &str, loc: Loc) -> Result<SessionStep, ParseError> {
6472        self.advance(); // consume `select` / `branch`
6473        self.consume(TokenType::LBrace)?;
6474        let mut branches = Vec::new();
6475        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6476            let label_tok = self.consume_any_ident_or_kw()?;
6477            self.consume(TokenType::Colon)?;
6478            let steps = self.parse_session_steps()?;
6479            branches.push(SessionBranch {
6480                label: label_tok.value,
6481                steps,
6482                loc: Loc { line: label_tok.line, column: label_tok.column },
6483            });
6484            if self.check(TokenType::Comma) {
6485                self.advance();
6486            }
6487        }
6488        self.consume(TokenType::RBrace)?;
6489        Ok(SessionStep { op: op.to_string(), branches, loc, ..Default::default() })
6490    }
6491
6492    /// Parse: `topology Name { nodes: [A, B, …]  edges: [A -> B : Session, …] }`.
6493    fn parse_topology(&mut self) -> Result<TopologyDefinition, ParseError> {
6494        let tok = self.consume(TokenType::Topology)?;
6495        let name = self.consume(TokenType::Identifier)?.value;
6496        let mut node = TopologyDefinition {
6497            name,
6498            nodes: Vec::new(),
6499            edges: Vec::new(),
6500            loc: Loc {
6501                line: tok.line,
6502                column: tok.column,
6503            },
6504            leading_trivia: Vec::new(),
6505            trailing_trivia: Vec::new(),
6506        };
6507        self.consume(TokenType::LBrace)?;
6508        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6509            let field_name = self.current().value.clone();
6510            self.advance();
6511            if !self.check(TokenType::Colon) {
6512                if self.check(TokenType::LBrace) {
6513                    self.skip_braced_block()?;
6514                }
6515                continue;
6516            }
6517            self.advance();
6518            match field_name.as_str() {
6519                "nodes" => node.nodes = self.parse_bracketed_identifiers()?,
6520                "edges" => node.edges = self.parse_topology_edges()?,
6521                _ => self.skip_value(),
6522            }
6523        }
6524        self.consume(TokenType::RBrace)?;
6525        Ok(node)
6526    }
6527
6528    fn parse_topology_edges(&mut self) -> Result<Vec<TopologyEdge>, ParseError> {
6529        self.consume(TokenType::LBracket)?;
6530        let mut edges = Vec::new();
6531        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
6532            edges.push(self.parse_topology_edge()?);
6533            if self.check(TokenType::Comma) {
6534                self.advance();
6535            }
6536        }
6537        self.consume(TokenType::RBracket)?;
6538        Ok(edges)
6539    }
6540
6541    fn parse_topology_edge(&mut self) -> Result<TopologyEdge, ParseError> {
6542        let src_tok = self.consume_any_ident_or_kw()?;
6543        self.consume(TokenType::Arrow)?;
6544        let tgt_tok = self.consume_any_ident_or_kw()?;
6545        self.consume(TokenType::Colon)?;
6546        let sess_tok = self.consume_any_ident_or_kw()?;
6547        Ok(TopologyEdge {
6548            source: src_tok.value,
6549            target: tgt_tok.value,
6550            session_ref: sess_tok.value,
6551            loc: Loc {
6552                line: src_tok.line,
6553                column: src_tok.column,
6554            },
6555        })
6556    }
6557
6558    // ── §λ-L-E Fase 5 — Cognitive immune system (paper_immune_v2.md) ────
6559
6560    /// Parse: `immune Name { watch, sensitivity, baseline, window, scope, tau, decay }`.
6561    fn parse_immune(&mut self) -> Result<ImmuneDefinition, ParseError> {
6562        let tok = self.consume(TokenType::Immune)?;
6563        let name = self.consume(TokenType::Identifier)?.value;
6564        let mut node = ImmuneDefinition {
6565            name,
6566            watch: Vec::new(),
6567            sensitivity: None,
6568            baseline: "learned".to_string(),
6569            window: 100,
6570            scope: String::new(),
6571            tau: String::new(),
6572            decay: "exponential".to_string(),
6573            loc: Loc {
6574                line: tok.line,
6575                column: tok.column,
6576            },
6577            leading_trivia: Vec::new(),
6578            trailing_trivia: Vec::new(),
6579        };
6580        self.consume(TokenType::LBrace)?;
6581        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6582            let field_name = self.current().value.clone();
6583            self.advance();
6584            if !self.check(TokenType::Colon) {
6585                if self.check(TokenType::LBrace) {
6586                    self.skip_braced_block()?;
6587                }
6588                continue;
6589            }
6590            self.advance();
6591            match field_name.as_str() {
6592                "watch" => node.watch = self.parse_bracketed_identifiers()?,
6593                "sensitivity" => node.sensitivity = self.parse_optional_float(),
6594                "baseline" => node.baseline = self.consume_any_ident_or_kw()?.value,
6595                "window" => {
6596                    if let Some(v) = self.parse_optional_int() {
6597                        node.window = v;
6598                    }
6599                }
6600                "scope" => {
6601                    let s_tok = self.consume_any_ident_or_kw()?;
6602                    let s = s_tok.value;
6603                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
6604                        return Err(ParseError {
6605                            message: format!(
6606                                "Invalid scope '{s}' in immune '{}' — \
6607                                 expected tenant | flow | global",
6608                                node.name
6609                            ),
6610                            line: s_tok.line,
6611                            column: s_tok.column,
6612                                                    ..Default::default()
6613                        });
6614                    }
6615                    node.scope = s;
6616                }
6617                "tau" => {
6618                    let t = self.current().clone();
6619                    match t.ttype {
6620                        TokenType::Duration | TokenType::StringLit => {
6621                            self.advance();
6622                            node.tau = t.value;
6623                        }
6624                        _ => node.tau = self.consume_any_ident_or_kw()?.value,
6625                    }
6626                }
6627                "decay" => {
6628                    let d_tok = self.consume_any_ident_or_kw()?;
6629                    let d = d_tok.value;
6630                    if !matches!(d.as_str(), "exponential" | "linear" | "none") {
6631                        return Err(ParseError {
6632                            message: format!(
6633                                "Invalid decay '{d}' in immune '{}' — \
6634                                 expected exponential | linear | none",
6635                                node.name
6636                            ),
6637                            line: d_tok.line,
6638                            column: d_tok.column,
6639                                                    ..Default::default()
6640                        });
6641                    }
6642                    node.decay = d;
6643                }
6644                _ => self.skip_value(),
6645            }
6646        }
6647        self.consume(TokenType::RBrace)?;
6648        Ok(node)
6649    }
6650
6651    /// Parse: `reflex Name { trigger, on_level, action, scope, sla }`.
6652    fn parse_reflex(&mut self) -> Result<ReflexDefinition, ParseError> {
6653        let tok = self.consume(TokenType::Reflex)?;
6654        let name = self.consume(TokenType::Identifier)?.value;
6655        let mut node = ReflexDefinition {
6656            name,
6657            trigger: String::new(),
6658            on_level: "doubt".to_string(),
6659            action: String::new(),
6660            scope: String::new(),
6661            sla: String::new(),
6662            loc: Loc {
6663                line: tok.line,
6664                column: tok.column,
6665            },
6666            leading_trivia: Vec::new(),
6667            trailing_trivia: Vec::new(),
6668        };
6669        self.consume(TokenType::LBrace)?;
6670        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6671            let field_name = self.current().value.clone();
6672            self.advance();
6673            if !self.check(TokenType::Colon) {
6674                if self.check(TokenType::LBrace) {
6675                    self.skip_braced_block()?;
6676                }
6677                continue;
6678            }
6679            self.advance();
6680            match field_name.as_str() {
6681                "trigger" => node.trigger = self.consume_any_ident_or_kw()?.value,
6682                "on_level" => {
6683                    let l_tok = self.consume_any_ident_or_kw()?;
6684                    let l = l_tok.value;
6685                    if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
6686                        return Err(ParseError {
6687                            message: format!(
6688                                "Invalid on_level '{l}' in reflex '{}' — \
6689                                 expected know | believe | speculate | doubt",
6690                                node.name
6691                            ),
6692                            line: l_tok.line,
6693                            column: l_tok.column,
6694                                                    ..Default::default()
6695                        });
6696                    }
6697                    node.on_level = l;
6698                }
6699                "action" => {
6700                    let a_tok = self.consume_any_ident_or_kw()?;
6701                    let a = a_tok.value;
6702                    if !matches!(
6703                        a.as_str(),
6704                        "drop"
6705                            | "revoke"
6706                            | "emit"
6707                            | "redact"
6708                            | "quarantine"
6709                            | "terminate"
6710                            | "alert"
6711                    ) {
6712                        return Err(ParseError {
6713                            message: format!(
6714                                "Invalid action '{a}' in reflex '{}' — \
6715                                 expected drop | revoke | emit | redact | \
6716                                 quarantine | terminate | alert",
6717                                node.name
6718                            ),
6719                            line: a_tok.line,
6720                            column: a_tok.column,
6721                                                    ..Default::default()
6722                        });
6723                    }
6724                    node.action = a;
6725                }
6726                "scope" => {
6727                    let s_tok = self.consume_any_ident_or_kw()?;
6728                    let s = s_tok.value;
6729                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
6730                        return Err(ParseError {
6731                            message: format!(
6732                                "Invalid scope '{s}' in reflex '{}' — \
6733                                 expected tenant | flow | global",
6734                                node.name
6735                            ),
6736                            line: s_tok.line,
6737                            column: s_tok.column,
6738                                                    ..Default::default()
6739                        });
6740                    }
6741                    node.scope = s;
6742                }
6743                "sla" => {
6744                    let t = self.current().clone();
6745                    match t.ttype {
6746                        TokenType::Duration | TokenType::StringLit => {
6747                            self.advance();
6748                            node.sla = t.value;
6749                        }
6750                        _ => node.sla = self.consume_any_ident_or_kw()?.value,
6751                    }
6752                }
6753                _ => self.skip_value(),
6754            }
6755        }
6756        self.consume(TokenType::RBrace)?;
6757        Ok(node)
6758    }
6759
6760    /// Parse: `heal Name { source, on_level, mode, scope, review_sla, shield, max_patches }`.
6761    fn parse_heal(&mut self) -> Result<HealDefinition, ParseError> {
6762        let tok = self.consume(TokenType::Heal)?;
6763        let name = self.consume(TokenType::Identifier)?.value;
6764        let mut node = HealDefinition {
6765            name,
6766            source: String::new(),
6767            on_level: "doubt".to_string(),
6768            mode: "human_in_loop".to_string(),
6769            scope: String::new(),
6770            review_sla: String::new(),
6771            shield_ref: String::new(),
6772            max_patches: 3,
6773            loc: Loc {
6774                line: tok.line,
6775                column: tok.column,
6776            },
6777            leading_trivia: Vec::new(),
6778            trailing_trivia: Vec::new(),
6779        };
6780        self.consume(TokenType::LBrace)?;
6781        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6782            let field_name = self.current().value.clone();
6783            self.advance();
6784            if !self.check(TokenType::Colon) {
6785                if self.check(TokenType::LBrace) {
6786                    self.skip_braced_block()?;
6787                }
6788                continue;
6789            }
6790            self.advance();
6791            match field_name.as_str() {
6792                "source" => node.source = self.consume_any_ident_or_kw()?.value,
6793                "on_level" => {
6794                    let l_tok = self.consume_any_ident_or_kw()?;
6795                    let l = l_tok.value;
6796                    if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
6797                        return Err(ParseError {
6798                            message: format!(
6799                                "Invalid on_level '{l}' in heal '{}' — \
6800                                 expected know | believe | speculate | doubt",
6801                                node.name
6802                            ),
6803                            line: l_tok.line,
6804                            column: l_tok.column,
6805                                                    ..Default::default()
6806                        });
6807                    }
6808                    node.on_level = l;
6809                }
6810                "mode" => {
6811                    let m_tok = self.consume_any_ident_or_kw()?;
6812                    let m = m_tok.value;
6813                    if !matches!(m.as_str(), "audit_only" | "human_in_loop" | "adversarial") {
6814                        return Err(ParseError {
6815                            message: format!(
6816                                "Invalid mode '{m}' in heal '{}' — \
6817                                 expected audit_only | human_in_loop | adversarial",
6818                                node.name
6819                            ),
6820                            line: m_tok.line,
6821                            column: m_tok.column,
6822                                                    ..Default::default()
6823                        });
6824                    }
6825                    node.mode = m;
6826                }
6827                "scope" => {
6828                    let s_tok = self.consume_any_ident_or_kw()?;
6829                    let s = s_tok.value;
6830                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
6831                        return Err(ParseError {
6832                            message: format!(
6833                                "Invalid scope '{s}' in heal '{}' — \
6834                                 expected tenant | flow | global",
6835                                node.name
6836                            ),
6837                            line: s_tok.line,
6838                            column: s_tok.column,
6839                                                    ..Default::default()
6840                        });
6841                    }
6842                    node.scope = s;
6843                }
6844                "review_sla" => {
6845                    let t = self.current().clone();
6846                    match t.ttype {
6847                        TokenType::Duration | TokenType::StringLit => {
6848                            self.advance();
6849                            node.review_sla = t.value;
6850                        }
6851                        _ => node.review_sla = self.consume_any_ident_or_kw()?.value,
6852                    }
6853                }
6854                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
6855                "max_patches" => {
6856                    if let Some(v) = self.parse_optional_int() {
6857                        node.max_patches = v;
6858                    }
6859                }
6860                _ => self.skip_value(),
6861            }
6862        }
6863        self.consume(TokenType::RBrace)?;
6864        Ok(node)
6865    }
6866
6867    // ── §λ-L-E Fase 9 — UI cognitiva (component / view) ────────────
6868
6869    /// Parse: `component Name { renders, via_shield, on_interact, render_hint }`.
6870    fn parse_component(&mut self) -> Result<ComponentDefinition, ParseError> {
6871        let tok = self.consume(TokenType::Component)?;
6872        let name = self.consume(TokenType::Identifier)?.value;
6873        let mut node = ComponentDefinition {
6874            name,
6875            renders: String::new(),
6876            via_shield: String::new(),
6877            on_interact: String::new(),
6878            render_hint: "custom".to_string(),
6879            loc: Loc {
6880                line: tok.line,
6881                column: tok.column,
6882            },
6883            leading_trivia: Vec::new(),
6884            trailing_trivia: Vec::new(),
6885        };
6886        self.consume(TokenType::LBrace)?;
6887        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6888            let field_name = self.current().value.clone();
6889            self.advance();
6890            if !self.check(TokenType::Colon) {
6891                if self.check(TokenType::LBrace) {
6892                    self.skip_braced_block()?;
6893                }
6894                continue;
6895            }
6896            self.advance();
6897            match field_name.as_str() {
6898                "renders" => node.renders = self.consume_any_ident_or_kw()?.value,
6899                "via_shield" => node.via_shield = self.consume_any_ident_or_kw()?.value,
6900                "on_interact" => node.on_interact = self.consume_any_ident_or_kw()?.value,
6901                "render_hint" => {
6902                    let h_tok = self.consume_any_ident_or_kw()?;
6903                    let h = h_tok.value;
6904                    if !matches!(h.as_str(), "card" | "list" | "form" | "chart" | "custom") {
6905                        return Err(ParseError {
6906                            message: format!(
6907                                "Invalid render_hint '{h}' in component '{}' — \
6908                                 expected card | list | form | chart | custom",
6909                                node.name
6910                            ),
6911                            line: h_tok.line,
6912                            column: h_tok.column,
6913                                                    ..Default::default()
6914                        });
6915                    }
6916                    node.render_hint = h;
6917                }
6918                _ => self.skip_value(),
6919            }
6920        }
6921        self.consume(TokenType::RBrace)?;
6922        Ok(node)
6923    }
6924
6925    /// Parse: `view Name { title, components: [...], route }`.
6926    fn parse_view(&mut self) -> Result<ViewDefinition, ParseError> {
6927        let tok = self.consume(TokenType::View)?;
6928        let name = self.consume(TokenType::Identifier)?.value;
6929        let mut node = ViewDefinition {
6930            name,
6931            title: String::new(),
6932            components: Vec::new(),
6933            route: String::new(),
6934            loc: Loc {
6935                line: tok.line,
6936                column: tok.column,
6937            },
6938            leading_trivia: Vec::new(),
6939            trailing_trivia: Vec::new(),
6940        };
6941        self.consume(TokenType::LBrace)?;
6942        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6943            let field_name = self.current().value.clone();
6944            self.advance();
6945            if !self.check(TokenType::Colon) {
6946                if self.check(TokenType::LBrace) {
6947                    self.skip_braced_block()?;
6948                }
6949                continue;
6950            }
6951            self.advance();
6952            match field_name.as_str() {
6953                "title" => node.title = self.consume(TokenType::StringLit)?.value,
6954                "components" => node.components = self.parse_bracketed_identifiers()?,
6955                "route" => node.route = self.consume(TokenType::StringLit)?.value,
6956                _ => self.skip_value(),
6957            }
6958        }
6959        self.consume(TokenType::RBrace)?;
6960        Ok(node)
6961    }
6962
6963    fn parse_axonendpoint(&mut self) -> Result<AxonEndpointDefinition, ParseError> {
6964        let tok = self.consume(TokenType::AxonEndpoint)?;
6965        let name = self.consume(TokenType::Identifier)?.value;
6966        let mut node = AxonEndpointDefinition {
6967            name,
6968            method: String::new(),
6969            path: String::new(),
6970            body_type: String::new(),
6971            execute_flow: String::new(),
6972            output_type: String::new(),
6973            shield_ref: String::new(),
6974            retries: None,
6975            timeout: String::new(),
6976            compliance: Vec::new(),
6977            // §Fase 30 — Defaults preserve backwards compat per D1.
6978            transport: "json".to_string(),
6979            keepalive: String::new(),
6980            // §Fase 31.b — Inference fields (parser-default state).
6981            // Both fields toggle/populate only when the source provides
6982            // an explicit `transport:` declaration (parser sets
6983            // `transport_explicit = true`) AND the type-checker walks
6984            // the program to compute `implicit_transport`.
6985            transport_explicit: false,
6986            implicit_transport: String::new(),
6987            // §Fase 32.g (D8) — auth scope; empty list ≡ no auth gate.
6988            requires_capabilities: Vec::new(),
6989            // §Fase 32.h — Replay-token binding (D9 plan-vivo).
6990            // Parser defaults: not explicit; effective value resolved
6991            // at deploy time using the method-default heuristic.
6992            replay_explicit: false,
6993            replay: false,
6994            // §Fase 33.z.k.b (v1.28.0) — Wire-format dialect default
6995            // empty; the runtime classifier resolves the default
6996            // dialect per the algebraic-effect predicate when the
6997            // source omits `transport: sse(<dialect>)`.
6998            transport_dialect: String::new(),
6999            // §Fase 33.z.k.1 (v1.27.1) — Algebraic-effect override.
7000            // Parser default false; populated by the type-checker's
7001            // compute_implicit_transports pass once the full program
7002            // is known (the predicate cross-references tool effects
7003            // declared anywhere in the program).
7004            has_algebraic_stream_effect: false,
7005            // §Fase 36.d (D2) — declared execution backend; empty ≡
7006            // not declared (the endpoint resolves down the Fase 36 D1
7007            // ladder). A non-empty value is validated against the
7008            // closed `AXONENDPOINT_BACKEND_VALUES` catalog below.
7009            backend: String::new(),
7010            // §Fase 37.y (D1) — Path-param names extracted from the
7011            // `path:` string AFTER the field is parsed. Initialized
7012            // empty; populated by `extract_path_param_names` after
7013            // the `path:` field is read in the loop below.
7014            path_params: Vec::new(),
7015            // §Fase 37.y (D2) — Inline `query: { name: Type, name: Type? }`
7016            // block. Initialized empty; populated by the `"query"` arm
7017            // in the field loop below. Closed catalog enforced at parse
7018            // time per `axonendpoint_is_valid_query_param_type`.
7019            query_params: Vec::new(),
7020            loc: Loc {
7021                line: tok.line,
7022                column: tok.column,
7023            },
7024            leading_trivia: Vec::new(),
7025            trailing_trivia: Vec::new(),
7026        };
7027        self.consume(TokenType::LBrace)?;
7028        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7029            let field_name = self.current().value.clone();
7030            self.advance();
7031            if self.check(TokenType::Colon) {
7032                self.advance();
7033                match field_name.as_str() {
7034                    "method" => {
7035                        // §Fase 32.b D3 — closed method enum
7036                        // `{GET, POST, PUT, DELETE, PATCH}`. Unknown
7037                        // values rejected at parse time with smart-
7038                        // suggest hint (Fase 28.e). HEAD/OPTIONS/etc.
7039                        // are runtime-managed and not adopter-
7040                        // declarable.
7041                        let value_tok = self.consume_any_ident_or_kw()?;
7042                        let value_upper = value_tok.value.to_uppercase();
7043                        if !axonendpoint_is_valid_method(&value_upper) {
7044                            let hint = crate::smart_suggest::suggest_for(
7045                                &value_upper,
7046                                AXONENDPOINT_METHOD_VALUES,
7047                            );
7048                            let base = format!(
7049                                "Invalid method '{}' in axonendpoint '{}'.",
7050                                value_tok.value, node.name
7051                            );
7052                            let message = if hint.is_empty() {
7053                                format!(
7054                                    "{base} expected GET | POST | PUT | DELETE | PATCH, found {}",
7055                                    value_tok.value
7056                                )
7057                            } else {
7058                                format!(
7059                                    "{base} {hint} (expected GET | POST | PUT | DELETE | PATCH, found {})",
7060                                    value_tok.value
7061                                )
7062                            };
7063                            return Err(ParseError {
7064                                message,
7065                                line: value_tok.line,
7066                                column: value_tok.column,
7067                                ..Default::default()
7068                            });
7069                        }
7070                        node.method = value_upper;
7071                    }
7072                    "path" => {
7073                        node.path = self.consume(TokenType::StringLit)?.value.clone();
7074                        // §Fase 37.y (D1) — extract `{name}` placeholders
7075                        // for the Request Binding Contract's path-param
7076                        // source. Duplicate `{name}` in the same path
7077                        // is rejected at parse time (HTTP route patterns
7078                        // structurally reject duplicates; surfacing the
7079                        // error here is friendlier than letting axum
7080                        // panic at registration).
7081                        match extract_path_param_names(&node.path) {
7082                            Ok(names) => node.path_params = names,
7083                            Err(dup) => {
7084                                let cur = self.current().clone();
7085                                return Err(ParseError {
7086                                    message: format!(
7087                                        "axonendpoint '{}' declares path '{}' \
7088                                         containing duplicate placeholder '{{{}}}'. \
7089                                         Each `{{name}}` in a `path:` must be \
7090                                         unique — the runtime cannot bind two \
7091                                         path segments to the same name (Fase 37.y D1).",
7092                                        node.name, node.path, dup,
7093                                    ),
7094                                    line: cur.line,
7095                                    column: cur.column,
7096                                    ..Default::default()
7097                                });
7098                            }
7099                        }
7100                    },
7101                    "body" => node.body_type = self.consume_any_ident_or_kw()?.value.clone(),
7102                    "query" => {
7103                        // §Fase 37.y (D2) — Inline query-parameter block.
7104                        // Grammar: `query: { name: Type [, name: Type?]* }`.
7105                        // Closed type catalog
7106                        // `AXONENDPOINT_QUERY_PARAM_TYPES = {Text, Int,
7107                        // Float, Bool, Uuid}`. Optional via `?` suffix
7108                        // reuses `TypeExpr.optional` semantics already in
7109                        // use for flow parameters + body type fields. A
7110                        // duplicate field name in the same block is a
7111                        // parse error (HTTP query strings DO allow
7112                        // multi-value but v1.38.5 binds the first value
7113                        // only — see plan vivo §7 forward-compat).
7114                        //
7115                        // §Fase 37.y (D2 robustness) — declaring `query:`
7116                        // twice on the same axonendpoint silently merged
7117                        // params pre-hardening. Now it's a parse error
7118                        // so an adopter typo / copy-paste mistake
7119                        // surfaces with line + column instead of
7120                        // producing an unexpectedly-augmented endpoint.
7121                        let lbrace_tok = self.consume(TokenType::LBrace)?;
7122                        let block_line = lbrace_tok.line;
7123                        if !node.query_params.is_empty() {
7124                            return Err(ParseError {
7125                                message: format!(
7126                                    "axonendpoint '{}' declares `query: {{ … }}` \
7127                                     more than once. The query-parameter block \
7128                                     is unique per endpoint; combine all params \
7129                                     into a single block (Fase 37.y D2).",
7130                                    node.name,
7131                                ),
7132                                line: lbrace_tok.line,
7133                                column: lbrace_tok.column,
7134                                ..Default::default()
7135                            });
7136                        }
7137                        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7138                            let name_tok = self.consume(TokenType::Identifier)?;
7139                            let field_name = name_tok.value.clone();
7140                            // Duplicate detection within the block.
7141                            if node
7142                                .query_params
7143                                .iter()
7144                                .any(|f| f.name == field_name)
7145                            {
7146                                return Err(ParseError {
7147                                    message: format!(
7148                                        "axonendpoint '{}' declares duplicate \
7149                                         query param '{}' inside `query: {{ … }}`. \
7150                                         Each name must appear at most once \
7151                                         (Fase 37.y D2).",
7152                                        node.name, field_name,
7153                                    ),
7154                                    line: name_tok.line,
7155                                    column: name_tok.column,
7156                                    ..Default::default()
7157                                });
7158                            }
7159                            self.consume(TokenType::Colon)?;
7160                            let type_expr = self.parse_type_expr()?;
7161                            // §Fase 37.y (D2 robustness) — reject generic
7162                            // type expressions on query params. The
7163                            // closed catalog is 5 primitives; container
7164                            // types (`Optional<T>`, `List<T>`, etc.)
7165                            // would mislead the adopter into thinking
7166                            // they bind multi-value query strings
7167                            // (deferred per plan vivo §7) or that
7168                            // `Optional<Text>` is the canonical way to
7169                            // declare an optional query (it's NOT —
7170                            // `Text?` is). Surface the canonical syntax
7171                            // verbatim so the fix is obvious.
7172                            if !type_expr.generic_param.is_empty() {
7173                                let canonical_hint = if type_expr.name == "Optional" {
7174                                    format!(
7175                                        " Use `{}?` (the `?` suffix) for an \
7176                                         optional query param instead of \
7177                                         `Optional<{}>`.",
7178                                        type_expr.generic_param,
7179                                        type_expr.generic_param,
7180                                    )
7181                                } else if type_expr.name == "List" {
7182                                    " Multi-value query params (e.g. `?tag=a&tag=b`) \
7183                                     are honest-deferred from v1.38.5; bind a \
7184                                     single-value `Text` query param and parse \
7185                                     the value inside the flow."
7186                                        .to_string()
7187                                } else {
7188                                    String::new()
7189                                };
7190                                return Err(ParseError {
7191                                    message: format!(
7192                                        "axonendpoint '{}' query param '{}' uses \
7193                                         a generic type `{}<{}>`. Query params \
7194                                         take a primitive type from the closed \
7195                                         catalog ({}); the `?` suffix marks \
7196                                         optional.{} (Fase 37.y D2).",
7197                                        node.name,
7198                                        field_name,
7199                                        type_expr.name,
7200                                        type_expr.generic_param,
7201                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | "),
7202                                        canonical_hint,
7203                                    ),
7204                                    line: type_expr.loc.line,
7205                                    column: type_expr.loc.column,
7206                                    ..Default::default()
7207                                });
7208                            }
7209                            // Validate against the closed catalog. A
7210                            // miss surfaces a Fase 28-style smart-suggest
7211                            // hint when within edit-distance 2.
7212                            if !axonendpoint_is_valid_query_param_type(&type_expr.name) {
7213                                // `smart_suggest::suggest_for` returns
7214                                // pre-formatted prose like
7215                                // "Did you mean `Text`?" or
7216                                // "Did you mean `Text` or `Int`?" (empty
7217                                // when no candidate within edit-distance
7218                                // 2). Concatenate without re-wrapping.
7219                                let hint = crate::smart_suggest::suggest_for(
7220                                    &type_expr.name,
7221                                    AXONENDPOINT_QUERY_PARAM_TYPES,
7222                                );
7223                                let hint_text = if hint.is_empty() {
7224                                    format!(
7225                                        " Expected one of: {}.",
7226                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | ")
7227                                    )
7228                                } else {
7229                                    format!(
7230                                        " {} Expected one of: {}.",
7231                                        hint,
7232                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | ")
7233                                    )
7234                                };
7235                                return Err(ParseError {
7236                                    message: format!(
7237                                        "axonendpoint '{}' query param '{}' has \
7238                                         unsupported type '{}'.{} (Fase 37.y D2).",
7239                                        node.name, field_name, type_expr.name,
7240                                        hint_text,
7241                                    ),
7242                                    line: type_expr.loc.line,
7243                                    column: type_expr.loc.column,
7244                                    ..Default::default()
7245                                });
7246                            }
7247                            node.query_params.push(TypeField {
7248                                name: field_name,
7249                                type_expr,
7250                                loc: Loc {
7251                                    line: name_tok.line,
7252                                    column: name_tok.column,
7253                                },
7254                            });
7255                            // Trailing comma is optional; the next loop
7256                            // iteration handles `}` cleanly. Accept both
7257                            // `name: Type, name: Type` AND `name: Type
7258                            // name: Type` (the existing parser style is
7259                            // forgiving about list separators).
7260                            if self.check(TokenType::Comma) {
7261                                self.advance();
7262                            }
7263                            let _ = block_line; // suppress unused warning
7264                        }
7265                        self.consume(TokenType::RBrace)?;
7266                    },
7267                    "execute" => node.execute_flow = self.consume_any_ident_or_kw()?.value.clone(),
7268                    "output" => {
7269                        // §Fase 38.x.f — promote axonendpoint `output:`
7270                        // parsing from a single token to the full
7271                        // generic-aware type expression (mirroring
7272                        // `parse_step` for FlowStep::Step which already
7273                        // uses `parse_output_type_string`).
7274                        //
7275                        // Pre-38.x.f: `output: List<Item>` captured only
7276                        // `"List"`, dropping `<Item>` (next tokens were
7277                        // either left unconsumed or absorbed by the
7278                        // following field). v1.39.0's narrow cardinality
7279                        // gate happened to fire correctly for `output: T`
7280                        // + retrieve-tail because the singular-detection
7281                        // path used `!starts_with("List<")` — but the
7282                        // SYMMETRIC `output: List<T>` + singular-tail
7283                        // case (38.x.f D3) needs the FULL `List<T>`
7284                        // shape captured; without it the gate sees
7285                        // `"List"` and misclassifies as Singular.
7286                        node.output_type = self.parse_output_type_string()?;
7287                    }
7288                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
7289                    "retries" => node.retries = self.parse_optional_int(),
7290                    "timeout" => {
7291                        let t = self.current().clone();
7292                        self.advance();
7293                        node.timeout = t.value.clone();
7294                    }
7295                    "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
7296                    "replay" => {
7297                        // §Fase 32.h (D9 plan-vivo) — Replay-token binding.
7298                        // Boolean `replay: true | false`. Default (when
7299                        // omitted) is method-derived at deploy-time:
7300                        // POST/PUT → true, GET/DELETE → false. Explicit
7301                        // declaration sets `replay_explicit = true` so
7302                        // the runtime knows NOT to override.
7303                        let value_tok = self.consume(TokenType::Bool)?;
7304                        node.replay = value_tok.value.eq_ignore_ascii_case("true");
7305                        node.replay_explicit = true;
7306                    }
7307                    "requires" => {
7308                        // §Fase 32.g (D8) — Auth scope per axonendpoint.
7309                        // Closed slug grammar
7310                        // `^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$` enforced
7311                        // at parse time with smart-suggest-style hint.
7312                        // Empty list means "no auth gate" (D9 backwards-
7313                        // compat). Cross-stack with Python parser.
7314                        let bracket_tok = self.current().clone();
7315                        let items = self.parse_bracketed_dot_identifiers()?;
7316                        for slug in &items {
7317                            if !is_valid_capability_slug(slug) {
7318                                return Err(ParseError {
7319                                    message: format!(
7320                                        "Invalid capability slug '{slug}' in axonendpoint '{}' \
7321                                         `requires:`. Capability slugs must match \
7322                                         ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
7323                                         lowercase identifiers starting with a letter. Examples: \
7324                                         `admin`, `legal.read`, `hipaa.phi.read`.",
7325                                        node.name
7326                                    ),
7327                                    line: bracket_tok.line,
7328                                    column: bracket_tok.column,
7329                                    ..Default::default()
7330                                });
7331                            }
7332                        }
7333                        node.requires_capabilities = items;
7334                    }
7335                    // §Fase 30.b — HTTP transport enum (D2 closed) + keepalive (D6 closed).
7336                    // Mirrors `axon/compiler/parser.py` `_parse_axonendpoint`.
7337                    // Drift-gate corpus verifies byte-identical parse cross-stack.
7338                    "transport" => {
7339                        let value_tok = self.consume_any_ident_or_kw()?;
7340                        let value = &value_tok.value;
7341                        if !axonendpoint_is_valid_transport(value) {
7342                            let hint = crate::smart_suggest::suggest_for(
7343                                value,
7344                                AXONENDPOINT_TRANSPORT_VALUES,
7345                            );
7346                            let base = format!(
7347                                "Invalid transport '{}' in axonendpoint '{}'.",
7348                                value, node.name
7349                            );
7350                            let message = if hint.is_empty() {
7351                                format!("{base} expected json | sse | ndjson, found {value}")
7352                            } else {
7353                                format!(
7354                                    "{base} {hint} (expected json | sse | ndjson, found {value})"
7355                                )
7356                            };
7357                            return Err(ParseError {
7358                                message,
7359                                line: value_tok.line,
7360                                column: value_tok.column,
7361                                ..Default::default()
7362                            });
7363                        }
7364                        node.transport = value.clone();
7365                        // §Fase 31.b D1 — mark the field as explicitly
7366                        // declared so the type-checker's implicit-transport
7367                        // inference knows NOT to override this value with
7368                        // the produces_stream-driven inference.
7369                        node.transport_explicit = true;
7370                        // §Fase 33.z.k.b (v1.28.0) — Optional dialect
7371                        // parametrization: `transport: sse(<dialect>)`.
7372                        // Only valid when the base value is `sse`
7373                        // (json + ndjson dialects are the dialects
7374                        // themselves; `json(<x>)` / `ndjson(<x>)`
7375                        // would be parse errors caught below).
7376                        if self.check(TokenType::LParen) {
7377                            if value != "sse" {
7378                                let tok = self.current().clone();
7379                                return Err(ParseError {
7380                                    message: format!(
7381                                        "Dialect parametrization \
7382                                         `transport: {value}(<dialect>)` is \
7383                                         only valid for `sse`; got \
7384                                         `{value}` in axonendpoint '{}'.",
7385                                        node.name
7386                                    ),
7387                                    line: tok.line,
7388                                    column: tok.column,
7389                                    ..Default::default()
7390                                });
7391                            }
7392                            self.advance(); // consume LParen
7393                            let dialect_tok = self.consume_any_ident_or_kw()?;
7394                            let dialect = dialect_tok.value.clone();
7395                            if !AXONENDPOINT_TRANSPORT_DIALECTS
7396                                .iter()
7397                                .any(|&d| d == dialect)
7398                            {
7399                                let hint = crate::smart_suggest::suggest_for(
7400                                    &dialect,
7401                                    AXONENDPOINT_TRANSPORT_DIALECTS,
7402                                );
7403                                let base = format!(
7404                                    "Invalid SSE dialect '{dialect}' in axonendpoint '{}'.",
7405                                    node.name
7406                                );
7407                                let message = if hint.is_empty() {
7408                                    format!(
7409                                        "{base} expected axon | openai | kimi | glm | anthropic, found {dialect}"
7410                                    )
7411                                } else {
7412                                    format!(
7413                                        "{base} {hint} (expected axon | openai | kimi | glm | anthropic, found {dialect})"
7414                                    )
7415                                };
7416                                return Err(ParseError {
7417                                    message,
7418                                    line: dialect_tok.line,
7419                                    column: dialect_tok.column,
7420                                    ..Default::default()
7421                                });
7422                            }
7423                            // Closing RParen.
7424                            let rparen_tok = self.current().clone();
7425                            if !self.check(TokenType::RParen) {
7426                                return Err(ParseError {
7427                                    message: format!(
7428                                        "Expected `)` after dialect name \
7429                                         in axonendpoint '{}' \
7430                                         (transport: sse(<dialect>) grammar).",
7431                                        node.name
7432                                    ),
7433                                    line: rparen_tok.line,
7434                                    column: rparen_tok.column,
7435                                    ..Default::default()
7436                                });
7437                            }
7438                            self.advance(); // consume RParen
7439                            node.transport_dialect = dialect;
7440                        }
7441                    }
7442                    "keepalive" => {
7443                        // Accepts either a DURATION token (e.g. `15s`) or
7444                        // an ident-like token. Validation against the
7445                        // closed enum {5s, 15s, 30s, 60s} happens after.
7446                        let value_tok = self.current().clone();
7447                        self.advance();
7448                        let value = &value_tok.value;
7449                        if !axonendpoint_is_valid_keepalive(value) {
7450                            let hint = crate::smart_suggest::suggest_for(
7451                                value,
7452                                AXONENDPOINT_KEEPALIVE_VALUES,
7453                            );
7454                            let base = format!(
7455                                "Invalid keepalive '{}' in axonendpoint '{}'.",
7456                                value, node.name
7457                            );
7458                            let message = if hint.is_empty() {
7459                                format!("{base} expected 5s | 15s | 30s | 60s, found {value}")
7460                            } else {
7461                                format!(
7462                                    "{base} {hint} (expected 5s | 15s | 30s | 60s, found {value})"
7463                                )
7464                            };
7465                            return Err(ParseError {
7466                                message,
7467                                line: value_tok.line,
7468                                column: value_tok.column,
7469                                ..Default::default()
7470                            });
7471                        }
7472                        node.keepalive = value.clone();
7473                    }
7474                    "backend" => {
7475                        // §Fase 36.d (D2) — declared execution backend.
7476                        // Closed catalog `CANONICAL_PROVIDERS ∪ {auto,
7477                        // stub}`; an unknown name is a parse error with
7478                        // a smart-suggest hint (the same discipline as
7479                        // `method`/`transport`/`keepalive`). The
7480                        // type-checker re-validates defensively for
7481                        // ASTs built outside the parser (LSP, tests).
7482                        let value_tok = self.consume_any_ident_or_kw()?;
7483                        let value = &value_tok.value;
7484                        if !axonendpoint_is_valid_backend(value) {
7485                            let hint = crate::smart_suggest::suggest_for(
7486                                value,
7487                                AXONENDPOINT_BACKEND_VALUES,
7488                            );
7489                            let expected = AXONENDPOINT_BACKEND_VALUES.join(" | ");
7490                            let base = format!(
7491                                "Invalid backend '{}' in axonendpoint '{}'.",
7492                                value, node.name
7493                            );
7494                            let message = if hint.is_empty() {
7495                                format!("{base} expected {expected}, found {value}")
7496                            } else {
7497                                format!(
7498                                    "{base} {hint} (expected {expected}, found {value})"
7499                                )
7500                            };
7501                            return Err(ParseError {
7502                                message,
7503                                line: value_tok.line,
7504                                column: value_tok.column,
7505                                ..Default::default()
7506                            });
7507                        }
7508                        node.backend = value.clone();
7509                    }
7510                    _ => self.skip_value(),
7511                }
7512            } else if self.check(TokenType::LBrace) {
7513                self.skip_braced_block()?;
7514            }
7515        }
7516        self.consume(TokenType::RBrace)?;
7517        Ok(node)
7518    }
7519
7520    // ── Numeric helpers for Tier 2 field parsing ────────────────────
7521
7522    fn parse_optional_int(&mut self) -> Option<i64> {
7523        let tok = self.current().clone();
7524        match tok.ttype {
7525            TokenType::Integer => {
7526                self.advance();
7527                tok.value.parse::<i64>().ok()
7528            }
7529            _ => {
7530                self.advance();
7531                None
7532            }
7533        }
7534    }
7535
7536    fn parse_optional_float(&mut self) -> Option<f64> {
7537        let tok = self.current().clone();
7538        match tok.ttype {
7539            TokenType::Float | TokenType::Integer => {
7540                self.advance();
7541                tok.value.parse::<f64>().ok()
7542            }
7543            _ => {
7544                self.advance();
7545                None
7546            }
7547        }
7548    }
7549
7550    // ── LAMBDA DATA (ΛD) ──────────────────────────────────────────
7551
7552    fn parse_lambda_data(&mut self) -> Result<LambdaDataDefinition, ParseError> {
7553        let tok = self.consume(TokenType::Lambda)?;
7554        let name = self.consume(TokenType::Identifier)?;
7555        self.consume(TokenType::LBrace)?;
7556
7557        let mut node = LambdaDataDefinition {
7558            name: name.value.clone(),
7559            ontology: String::new(),
7560            certainty: 1.0,
7561            temporal_frame_start: String::new(),
7562            temporal_frame_end: String::new(),
7563            provenance: String::new(),
7564            derivation: String::new(),
7565            loc: Loc {
7566                line: tok.line,
7567                column: tok.column,
7568            },
7569            leading_trivia: Vec::new(),
7570            trailing_trivia: Vec::new(),
7571        };
7572
7573        while !self.check(TokenType::RBrace) {
7574            let field = self.current().clone();
7575            match field.ttype {
7576                TokenType::Ontology => {
7577                    self.advance();
7578                    self.consume(TokenType::Colon)?;
7579                    node.ontology = self.consume(TokenType::StringLit)?.value.clone();
7580                }
7581                TokenType::Certainty => {
7582                    self.advance();
7583                    self.consume(TokenType::Colon)?;
7584                    let val = self.current().clone();
7585                    match val.ttype {
7586                        TokenType::Float => {
7587                            self.advance();
7588                            node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
7589                        }
7590                        TokenType::Integer => {
7591                            self.advance();
7592                            node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
7593                        }
7594                        _ => {
7595                            return Err(ParseError {
7596                                message: format!(
7597                                    "Expected number for certainty, got '{}'",
7598                                    val.value
7599                                ),
7600                                line: val.line,
7601                                column: val.column,
7602                                                            ..Default::default()
7603                            });
7604                        }
7605                    }
7606                }
7607                TokenType::TemporalFrame => {
7608                    self.advance();
7609                    self.consume(TokenType::Colon)?;
7610                    node.temporal_frame_start = self.consume(TokenType::StringLit)?.value.clone();
7611                    // Optional second string for end frame
7612                    if self.check(TokenType::StringLit) {
7613                        node.temporal_frame_end = self.consume(TokenType::StringLit)?.value.clone();
7614                    }
7615                }
7616                TokenType::Provenance => {
7617                    self.advance();
7618                    self.consume(TokenType::Colon)?;
7619                    node.provenance = self.consume(TokenType::StringLit)?.value.clone();
7620                }
7621                TokenType::Derivation => {
7622                    self.advance();
7623                    self.consume(TokenType::Colon)?;
7624                    let d = self.current().clone();
7625                    self.advance();
7626                    node.derivation = d.value.clone();
7627                }
7628                _ => {
7629                    // Skip unknown fields gracefully
7630                    self.advance();
7631                    if self.check(TokenType::Colon) {
7632                        self.advance();
7633                        self.skip_value();
7634                    }
7635                }
7636            }
7637        }
7638
7639        self.consume(TokenType::RBrace)?;
7640        Ok(node)
7641    }
7642
7643    fn parse_lambda_data_apply(&mut self) -> Result<LambdaDataApplyNode, ParseError> {
7644        let tok = self.consume(TokenType::Lambda)?;
7645        let lambda_name = self.consume(TokenType::Identifier)?;
7646
7647        // Expect "on" keyword (parsed as identifier since it's not reserved)
7648        let on_tok = self.current().clone();
7649        self.advance();
7650        if on_tok.value != "on" {
7651            return Err(ParseError {
7652                message: format!(
7653                    "Expected 'on' after lambda data name in flow step, got '{}'",
7654                    on_tok.value
7655                ),
7656                line: on_tok.line,
7657                column: on_tok.column,
7658                            ..Default::default()
7659            });
7660        }
7661
7662        let target = self.current().clone();
7663        self.advance();
7664
7665        let mut output_type = String::new();
7666        if self.check(TokenType::Arrow) {
7667            self.advance();
7668            output_type = self.consume(TokenType::Identifier)?.value.clone();
7669        }
7670
7671        Ok(LambdaDataApplyNode {
7672            lambda_data_name: lambda_name.value.clone(),
7673            target: target.value.clone(),
7674            output_type,
7675            loc: Loc {
7676                line: tok.line,
7677                column: tok.column,
7678            },
7679        })
7680    }
7681
7682    // ── GENERIC (Tier 2+) ────────────────────────────────────────
7683
7684    fn parse_generic_declaration(&mut self) -> Result<Declaration, ParseError> {
7685        let kw_tok = self.current().clone();
7686        self.advance(); // consume keyword
7687
7688        // Try to consume a name (identifier or keyword-as-name)
7689        let name = if self.current().ttype == TokenType::Identifier {
7690            let n = self.current().value.clone();
7691            self.advance();
7692            n
7693        } else if !self.check(TokenType::LBrace)
7694            && !self.check(TokenType::LParen)
7695            && !self.check(TokenType::Eof)
7696            && self
7697                .current()
7698                .value
7699                .chars()
7700                .all(|c| c.is_alphanumeric() || c == '_')
7701        {
7702            let n = self.current().value.clone();
7703            self.advance();
7704            n
7705        } else {
7706            String::new()
7707        };
7708
7709        // Skip optional parens: (...)
7710        if self.check(TokenType::LParen) {
7711            self.advance();
7712            let mut depth = 1u32;
7713            while depth > 0 && !self.check(TokenType::Eof) {
7714                if self.check(TokenType::LParen) {
7715                    depth += 1;
7716                } else if self.check(TokenType::RParen) {
7717                    depth -= 1;
7718                }
7719                self.advance();
7720            }
7721        }
7722
7723        // Skip tokens until LBrace or next declaration
7724        while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
7725            if self.check(TokenType::Eof) {
7726                break;
7727            }
7728            self.advance();
7729        }
7730
7731        // Skip braced block if present
7732        if self.check(TokenType::LBrace) {
7733            self.skip_braced_block()?;
7734        }
7735
7736        Ok(Declaration::Generic(GenericDeclaration {
7737            keyword: kw_tok.value,
7738            name,
7739            loc: Loc {
7740                line: kw_tok.line,
7741                column: kw_tok.column,
7742            },
7743            leading_trivia: Vec::new(),
7744            trailing_trivia: Vec::new(),
7745        }))
7746    }
7747
7748    // ──────────────────────────────────────────────────────────────────
7749    //  §λ-L-E Fase 13 — Mobile Typed Channels parsers
7750    //  (paper_mobile_channels.md §3 + plan/fase_13)
7751    //  Direct port of axon/compiler/parser.py:_parse_channel/emit/publish/discover.
7752    // ──────────────────────────────────────────────────────────────────
7753
7754    /// Parse: `channel Name { message, qos, lifetime, persistence, shield }`.
7755    fn parse_channel(&mut self) -> Result<ChannelDefinition, ParseError> {
7756        let tok = self.consume(TokenType::Channel)?;
7757        let name = self.consume(TokenType::Identifier)?.value;
7758        let mut node = ChannelDefinition {
7759            name: name.clone(),
7760            message: String::new(),
7761            qos: "at_least_once".to_string(),
7762            lifetime: "affine".to_string(),
7763            persistence: "ephemeral".to_string(),
7764            shield_ref: String::new(),
7765            loc: Loc {
7766                line: tok.line,
7767                column: tok.column,
7768            },
7769            leading_trivia: Vec::new(),
7770            trailing_trivia: Vec::new(),
7771        };
7772        self.consume(TokenType::LBrace)?;
7773        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7774            let field_tok = self.current().clone();
7775            let field_name = field_tok.value.clone();
7776            self.advance();
7777            if !self.check(TokenType::Colon) {
7778                if self.check(TokenType::LBrace) {
7779                    self.skip_braced_block()?;
7780                }
7781                continue;
7782            }
7783            self.advance();
7784            match field_name.as_str() {
7785                "message" => node.message = self.parse_channel_message_type()?,
7786                "qos" => {
7787                    let q_tok = self.consume_any_ident_or_kw()?;
7788                    if !matches!(
7789                        q_tok.value.as_str(),
7790                        "at_most_once" | "at_least_once" | "exactly_once" | "broadcast" | "queue"
7791                    ) {
7792                        return Err(ParseError {
7793                            message: format!(
7794                                "Invalid qos '{}' in channel '{}' — \
7795                                 expected at_most_once | at_least_once | \
7796                                 exactly_once | broadcast | queue",
7797                                q_tok.value, name
7798                            ),
7799                            line: q_tok.line,
7800                            column: q_tok.column,
7801                                                    ..Default::default()
7802                        });
7803                    }
7804                    node.qos = q_tok.value;
7805                }
7806                "lifetime" => {
7807                    let lt_tok = self.consume_any_ident_or_kw()?;
7808                    if !matches!(lt_tok.value.as_str(), "linear" | "affine" | "persistent") {
7809                        return Err(ParseError {
7810                            message: format!(
7811                                "Invalid lifetime '{}' in channel '{}' — \
7812                                 expected linear | affine | persistent",
7813                                lt_tok.value, name
7814                            ),
7815                            line: lt_tok.line,
7816                            column: lt_tok.column,
7817                                                    ..Default::default()
7818                        });
7819                    }
7820                    node.lifetime = lt_tok.value;
7821                }
7822                "persistence" => {
7823                    let p_tok = self.consume_any_ident_or_kw()?;
7824                    if !matches!(p_tok.value.as_str(), "ephemeral" | "persistent_axonstore") {
7825                        return Err(ParseError {
7826                            message: format!(
7827                                "Invalid persistence '{}' in channel '{}' — \
7828                                 expected ephemeral | persistent_axonstore",
7829                                p_tok.value, name
7830                            ),
7831                            line: p_tok.line,
7832                            column: p_tok.column,
7833                                                    ..Default::default()
7834                        });
7835                    }
7836                    node.persistence = p_tok.value;
7837                }
7838                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
7839                _ => self.skip_value(),
7840            }
7841        }
7842        self.consume(TokenType::RBrace)?;
7843        Ok(node)
7844    }
7845
7846    /// Parse a `message:` value, supporting nested `Channel<…>`
7847    /// (second-order session types — paper §3.3).
7848    fn parse_channel_message_type(&mut self) -> Result<String, ParseError> {
7849        let head = self.consume(TokenType::Identifier)?;
7850        let mut spelling = head.value;
7851        if self.check(TokenType::Lt) {
7852            self.advance();
7853            let inner = self.parse_channel_message_type()?;
7854            self.consume(TokenType::Gt)?;
7855            spelling = format!("{}<{}>", spelling, inner);
7856        }
7857        Ok(spelling)
7858    }
7859
7860    /// Parse: `emit ChannelName(value_ref)` — Chan-Output / Chan-Mobility.
7861    ///
7862    /// `value_ref` accepts a bare identifier (variable / channel name for
7863    /// mobility) or a dotted path (`Step.output.field`) referencing a prior
7864    /// step result (Fase 13.i — runtime resolves via ContextManager).
7865    fn parse_emit_step(&mut self) -> Result<FlowStep, ParseError> {
7866        let tok = self.consume(TokenType::Emit)?;
7867        let channel = self.consume(TokenType::Identifier)?.value;
7868        self.consume(TokenType::LParen)?;
7869        let value = self.parse_emit_value_ref()?;
7870        self.consume(TokenType::RParen)?;
7871        Ok(FlowStep::Emit(EmitStatement {
7872            channel_ref: channel,
7873            value_ref: value,
7874            loc: Loc {
7875                line: tok.line,
7876                column: tok.column,
7877            },
7878        }))
7879    }
7880
7881    /// Parse: `IDENTIFIER ('.' (IDENTIFIER | keyword))*` → dot-joined string
7882    /// (Fase 13.i).
7883    ///
7884    /// Mirrors the Python `_parse_emit_value_ref` helper exactly so the IR
7885    /// JSON for `emit Hello(Build.output)` is byte-identical between the
7886    /// two reference implementations.
7887    ///
7888    /// The HEAD must be a real ``Identifier``. Subsequent segments after a
7889    /// `.` may be identifiers OR keywords — common field names like
7890    /// ``output``, ``result``, ``message``, ``state``, etc. are reserved
7891    /// words in Axon but adopters must be able to write them as
7892    /// dotted-access segments. The accepting predicate:
7893    ///   - the lexer carried a non-empty `value` (every Word-like token does)
7894    ///   - the value's first byte is a letter or underscore (filters out
7895    ///     punctuation tokens such as ',', '{', etc.)
7896    fn parse_emit_value_ref(&mut self) -> Result<String, ParseError> {
7897        let head = self.consume(TokenType::Identifier)?.value;
7898        let mut parts = vec![head];
7899        while self.check(TokenType::Dot) {
7900            self.advance(); // consume '.'
7901            let next_tok = self.current().clone();
7902            let valid = !next_tok.value.is_empty()
7903                && next_tok.value.as_bytes()[0].is_ascii_alphabetic()
7904                || next_tok.value.starts_with('_');
7905            if !valid {
7906                return Err(ParseError {
7907                    message: format!(
7908                        "Expected identifier or keyword after '.' in dotted \
7909                         access, found {:?}",
7910                        next_tok.value
7911                    ),
7912                    line: next_tok.line,
7913                    column: next_tok.column,
7914                                    ..Default::default()
7915                });
7916            }
7917            self.advance();
7918            parts.push(next_tok.value);
7919        }
7920        Ok(parts.join("."))
7921    }
7922
7923    /// Parse: `publish ChannelName within ShieldName` — Publish-Ext (D8).
7924    fn parse_publish_step(&mut self) -> Result<FlowStep, ParseError> {
7925        let tok = self.consume(TokenType::Publish)?;
7926        let channel = self.consume(TokenType::Identifier)?.value;
7927        self.consume(TokenType::Within)?;
7928        let shield = self.consume(TokenType::Identifier)?.value;
7929        Ok(FlowStep::Publish(PublishStatement {
7930            channel_ref: channel,
7931            shield_ref: shield,
7932            loc: Loc {
7933                line: tok.line,
7934                column: tok.column,
7935            },
7936        }))
7937    }
7938
7939    /// Parse: `discover ChannelName as alias` — dual of publish.
7940    fn parse_discover_step(&mut self) -> Result<FlowStep, ParseError> {
7941        let tok = self.consume(TokenType::Discover)?;
7942        let cap = self.consume(TokenType::Identifier)?.value;
7943        self.consume(TokenType::As)?;
7944        let alias = self.consume(TokenType::Identifier)?.value;
7945        Ok(FlowStep::Discover(DiscoverStatement {
7946            capability_ref: cap,
7947            alias,
7948            loc: Loc {
7949                line: tok.line,
7950                column: tok.column,
7951            },
7952        }))
7953    }
7954}
7955
7956// ── §λ-L-E Fase 13 — Mobile Typed Channels parser tests ─────────────────────
7957
7958#[cfg(test)]
7959mod fase13_parser_tests {
7960    use super::*;
7961    use crate::lexer::Lexer;
7962
7963    fn parse(src: &str) -> Result<Program, ParseError> {
7964        let tokens = Lexer::new(src, "<test>").tokenize().expect("lex");
7965        Parser::new(tokens).parse()
7966    }
7967
7968    #[test]
7969    fn channel_full_parses() {
7970        let src = r#"channel C { message: Order qos: at_least_once lifetime: affine persistence: ephemeral shield: Gate }"#;
7971        let prog = parse(src).expect("parse");
7972        match &prog.declarations[0] {
7973            Declaration::Channel(c) => {
7974                assert_eq!(c.name, "C");
7975                assert_eq!(c.message, "Order");
7976                assert_eq!(c.qos, "at_least_once");
7977                assert_eq!(c.lifetime, "affine");
7978                assert_eq!(c.persistence, "ephemeral");
7979                assert_eq!(c.shield_ref, "Gate");
7980            }
7981            _ => panic!("expected ChannelDefinition"),
7982        }
7983    }
7984
7985    #[test]
7986    fn channel_defaults_match_paper_d1() {
7987        let prog = parse("channel C { message: Order }").expect("parse");
7988        if let Declaration::Channel(c) = &prog.declarations[0] {
7989            assert_eq!(c.qos, "at_least_once"); // default
7990            assert_eq!(c.lifetime, "affine"); // D1 default
7991            assert_eq!(c.persistence, "ephemeral");
7992            assert_eq!(c.shield_ref, "");
7993        } else {
7994            panic!("expected ChannelDefinition");
7995        }
7996    }
7997
7998    #[test]
7999    fn channel_second_order_message_type_parses() {
8000        let prog = parse("channel C { message: Channel<Order> }").expect("parse");
8001        if let Declaration::Channel(c) = &prog.declarations[0] {
8002            assert_eq!(c.message, "Channel<Order>");
8003        } else {
8004            panic!("expected ChannelDefinition");
8005        }
8006    }
8007
8008    #[test]
8009    fn channel_nested_channel_message_type_parses() {
8010        let prog = parse("channel C { message: Channel<Channel<Order>> }").expect("parse");
8011        if let Declaration::Channel(c) = &prog.declarations[0] {
8012            assert_eq!(c.message, "Channel<Channel<Order>>");
8013        } else {
8014            panic!("expected ChannelDefinition");
8015        }
8016    }
8017
8018    #[test]
8019    fn channel_invalid_qos_rejected() {
8020        let err = parse("channel C { message: T qos: bogus }").unwrap_err();
8021        assert!(err.message.contains("Invalid qos"), "got {}", err.message);
8022    }
8023
8024    #[test]
8025    fn channel_invalid_lifetime_rejected() {
8026        let err = parse("channel C { message: T lifetime: eternal }").unwrap_err();
8027        assert!(
8028            err.message.contains("Invalid lifetime"),
8029            "got {}",
8030            err.message
8031        );
8032    }
8033
8034    #[test]
8035    fn channel_invalid_persistence_rejected() {
8036        let err = parse("channel C { message: T persistence: forever }").unwrap_err();
8037        assert!(
8038            err.message.contains("Invalid persistence"),
8039            "got {}",
8040            err.message
8041        );
8042    }
8043
8044    #[test]
8045    fn emit_value_parses() {
8046        let src = "flow f() -> Out { emit C(payload) }";
8047        let prog = parse(src).expect("parse");
8048        if let Declaration::Flow(f) = &prog.declarations[0] {
8049            match &f.body[0] {
8050                FlowStep::Emit(e) => {
8051                    assert_eq!(e.channel_ref, "C");
8052                    assert_eq!(e.value_ref, "payload");
8053                }
8054                other => panic!("expected Emit, got {:?}", other),
8055            }
8056        } else {
8057            panic!("expected Flow");
8058        }
8059    }
8060
8061    #[test]
8062    fn publish_within_shield_parses() {
8063        let src = "flow f() -> Cap { publish C within Gate }";
8064        let prog = parse(src).expect("parse");
8065        if let Declaration::Flow(f) = &prog.declarations[0] {
8066            match &f.body[0] {
8067                FlowStep::Publish(p) => {
8068                    assert_eq!(p.channel_ref, "C");
8069                    assert_eq!(p.shield_ref, "Gate");
8070                }
8071                other => panic!("expected Publish, got {:?}", other),
8072            }
8073        } else {
8074            panic!("expected Flow");
8075        }
8076    }
8077
8078    #[test]
8079    fn discover_with_alias_parses() {
8080        let src = "flow f() -> Out { discover C as ch }";
8081        let prog = parse(src).expect("parse");
8082        if let Declaration::Flow(f) = &prog.declarations[0] {
8083            match &f.body[0] {
8084                FlowStep::Discover(d) => {
8085                    assert_eq!(d.capability_ref, "C");
8086                    assert_eq!(d.alias, "ch");
8087                }
8088                other => panic!("expected Discover, got {:?}", other),
8089            }
8090        } else {
8091            panic!("expected Flow");
8092        }
8093    }
8094
8095    #[test]
8096    fn listen_typed_ref_sets_flag_true() {
8097        let src = "daemon D() { goal: \"x\" listen C as ev { } }";
8098        let prog = parse(src).expect("parse");
8099        if let Declaration::Daemon(d) = &prog.declarations[0] {
8100            assert_eq!(d.listeners.len(), 1);
8101            assert_eq!(d.listeners[0].channel, "C");
8102            assert!(d.listeners[0].channel_is_ref, "typed ref ⇒ true");
8103        } else {
8104            panic!("expected Daemon");
8105        }
8106    }
8107
8108    #[test]
8109    fn listen_string_topic_legacy_flag_false() {
8110        let src = "daemon D() { goal: \"x\" listen \"orders\" as ev { } }";
8111        let prog = parse(src).expect("parse");
8112        if let Declaration::Daemon(d) = &prog.declarations[0] {
8113            assert_eq!(d.listeners.len(), 1);
8114            assert_eq!(d.listeners[0].channel, "orders");
8115            assert!(!d.listeners[0].channel_is_ref, "string topic ⇒ false");
8116        } else {
8117            panic!("expected Daemon");
8118        }
8119    }
8120
8121    // ── Fase 13.i — emit value_ref accepts dotted access ───────────
8122
8123    fn extract_first_emit(prog: &Program) -> &EmitStatement {
8124        if let Declaration::Flow(f) = &prog.declarations[0] {
8125            if let FlowStep::Emit(e) = &f.body[0] {
8126                return e;
8127            }
8128        }
8129        panic!("expected emit statement at flow body[0]");
8130    }
8131
8132    #[test]
8133    fn emit_accepts_bare_identifier_value_ref() {
8134        // Pre-13.i baseline — must keep working.
8135        let prog = parse("flow f() -> Out { emit Hello(payload) }").expect("parse");
8136        let emit = extract_first_emit(&prog);
8137        assert_eq!(emit.channel_ref, "Hello");
8138        assert_eq!(emit.value_ref, "payload");
8139    }
8140
8141    #[test]
8142    fn emit_accepts_two_segment_dotted_value_ref() {
8143        // The exact case adopters reported as broken before 13.i.
8144        let prog = parse("flow f() -> Out { emit Hello(Build.output) }").expect("parse");
8145        let emit = extract_first_emit(&prog);
8146        assert_eq!(emit.value_ref, "Build.output");
8147    }
8148
8149    #[test]
8150    fn emit_accepts_three_segment_nested_dotted_value_ref() {
8151        let prog = parse("flow f() -> Out { emit Score(Analyze.result.score) }").expect("parse");
8152        let emit = extract_first_emit(&prog);
8153        assert_eq!(emit.value_ref, "Analyze.result.score");
8154    }
8155
8156    #[test]
8157    fn emit_dotted_with_trailing_dot_fails() {
8158        // Trailing `.` must still error — every '.' demands an identifier.
8159        let result = parse("flow f() -> Out { emit Hello(Build.) }");
8160        assert!(result.is_err(), "expected parse error for trailing dot");
8161    }
8162}
8163
8164// ── §Fase 14.a — declaration_trivia parallel channel tests ──────────────────
8165
8166#[cfg(test)]
8167mod fase14a_declaration_trivia_tests {
8168    use super::*;
8169    use crate::lexer::Lexer;
8170    use crate::tokens::TriviaKind;
8171
8172    fn parse(src: &str) -> Program {
8173        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
8174        Parser::new(toks).parse().expect("parse")
8175    }
8176
8177    #[test]
8178    fn no_comments_means_empty_trivia_per_decl() {
8179        let prog = parse("flow F() -> Out { }");
8180        assert_eq!(prog.declarations.len(), 1);
8181        assert_eq!(prog.declaration_trivia.len(), 1);
8182        assert!(prog.declaration_trivia[0].leading.is_empty());
8183        assert!(prog.declaration_trivia[0].trailing.is_empty());
8184    }
8185
8186    #[test]
8187    fn doc_line_comment_attaches_as_leading() {
8188        let prog = parse("/// Documents F\nflow F() -> Out { }");
8189        let triv = &prog.declaration_trivia[0];
8190        assert_eq!(triv.leading.len(), 1);
8191        assert_eq!(triv.leading[0].kind, TriviaKind::DocLine);
8192        assert!(triv.leading[0].is_doc());
8193        assert_eq!(triv.leading[0].text, "/// Documents F");
8194    }
8195
8196    #[test]
8197    fn regular_line_comment_attaches_as_leading() {
8198        let prog = parse("// header\nflow F() -> Out { }");
8199        let triv = &prog.declaration_trivia[0];
8200        assert_eq!(triv.leading.len(), 1);
8201        assert_eq!(triv.leading[0].kind, TriviaKind::Line);
8202        assert!(!triv.leading[0].is_doc());
8203    }
8204
8205    #[test]
8206    fn block_doc_comment_attaches_as_leading() {
8207        let prog = parse("/** Doc block */\nflow F() -> Out { }");
8208        let triv = &prog.declaration_trivia[0];
8209        assert_eq!(triv.leading[0].kind, TriviaKind::DocBlock);
8210        assert!(triv.leading[0].is_doc());
8211    }
8212
8213    #[test]
8214    fn multiple_comments_collected_in_source_order() {
8215        let src = "/// First\n/// Second\nflow F() -> Out { }";
8216        let prog = parse(src);
8217        let triv = &prog.declaration_trivia[0];
8218        assert_eq!(triv.leading.len(), 2);
8219        assert_eq!(triv.leading[0].text, "/// First");
8220        assert_eq!(triv.leading[1].text, "/// Second");
8221    }
8222
8223    #[test]
8224    fn three_decls_each_get_own_leading() {
8225        let src = "/// for A\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }\n/// for C\nflow C() -> Out { }";
8226        let prog = parse(src);
8227        assert_eq!(prog.declarations.len(), 3);
8228        assert_eq!(prog.declaration_trivia.len(), 3);
8229        for (idx, name) in ["A", "B", "C"].iter().enumerate() {
8230            let triv = &prog.declaration_trivia[idx];
8231            assert_eq!(triv.leading.len(), 1);
8232            assert_eq!(triv.leading[0].text, format!("/// for {name}"));
8233        }
8234    }
8235
8236    #[test]
8237    fn trailing_comment_attaches_to_last_token_of_decl() {
8238        // Comment on the same line as the decl's closing brace.
8239        let prog = parse("flow F() -> Out { } // tail");
8240        let triv = &prog.declaration_trivia[0];
8241        assert_eq!(triv.trailing.len(), 1);
8242        assert_eq!(triv.trailing[0].text, "// tail");
8243    }
8244
8245    #[test]
8246    fn mixed_doc_and_regular_preserve_order_between_decls() {
8247        let src = "/// doc for A\nflow A() -> Out { }\n\n// header line\n/// doc for B\nflow B() -> Out { }";
8248        let prog = parse(src);
8249        assert_eq!(prog.declarations.len(), 2);
8250        // A: just the doc comment.
8251        assert_eq!(prog.declaration_trivia[0].leading.len(), 1);
8252        // B: header + doc, in source order.
8253        assert_eq!(prog.declaration_trivia[1].leading.len(), 2);
8254        assert_eq!(prog.declaration_trivia[1].leading[0].text, "// header line");
8255        assert_eq!(prog.declaration_trivia[1].leading[1].text, "/// doc for B");
8256    }
8257
8258    #[test]
8259    fn parser_unaffected_by_comments_in_grammar_path() {
8260        // The parser must accept comments interleaved between every
8261        // legal token without affecting the AST shape it produces.
8262        // This is the regression guard for "lossless lexing must not
8263        // change parsing semantics."
8264        let src =
8265            "// before flow\nflow /* between flow and name */ F() -> Out {\n  // body comment\n}";
8266        let prog = parse(src);
8267        assert_eq!(prog.declarations.len(), 1);
8268        if let Declaration::Flow(f) = &prog.declarations[0] {
8269            assert_eq!(f.name, "F");
8270        } else {
8271            panic!("expected Flow declaration");
8272        }
8273    }
8274}
8275
8276// ── §Fase 14.b — per-struct trivia fields tests ─────────────────────────────
8277//
8278// 14.b spreads `leading_trivia` / `trailing_trivia` into every Declaration
8279// variant struct (FlowDefinition, ChannelDefinition, PersonaDefinition, …).
8280// The Python AST already had this shape since 14.a; 14.b achieves Rust
8281// parity. The side-channel `Program.declaration_trivia` is preserved for
8282// backward compat — these tests verify the new direct access path.
8283
8284#[cfg(test)]
8285mod fase14b_per_struct_trivia_tests {
8286    use super::*;
8287    use crate::lexer::Lexer;
8288    use crate::tokens::TriviaKind;
8289
8290    fn parse(src: &str) -> Program {
8291        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
8292        Parser::new(toks).parse().expect("parse")
8293    }
8294
8295    #[test]
8296    fn flow_definition_carries_leading_trivia_directly() {
8297        let prog = parse("/// documents F\nflow F() -> Out { }");
8298        if let Declaration::Flow(f) = &prog.declarations[0] {
8299            assert_eq!(f.leading_trivia.len(), 1);
8300            assert_eq!(f.leading_trivia[0].kind, TriviaKind::DocLine);
8301            assert_eq!(f.leading_trivia[0].text, "/// documents F");
8302            assert!(f.trailing_trivia.is_empty());
8303        } else {
8304            panic!("expected Flow declaration");
8305        }
8306    }
8307
8308    #[test]
8309    fn flow_definition_carries_trailing_trivia_directly() {
8310        let prog = parse("flow F() -> Out { } // tail comment");
8311        if let Declaration::Flow(f) = &prog.declarations[0] {
8312            assert_eq!(f.trailing_trivia.len(), 1);
8313            assert_eq!(f.trailing_trivia[0].text, "// tail comment");
8314        } else {
8315            panic!("expected Flow declaration");
8316        }
8317    }
8318
8319    #[test]
8320    fn channel_definition_carries_trivia_directly() {
8321        // ChannelDefinition is a Tier-1 declaration; verify per-struct fields
8322        // populate just like FlowDefinition.
8323        let src = concat!(
8324            "/// inbound order events\n",
8325            "channel Orders {\n",
8326            "    message:     Order\n",
8327            "    qos:         at_least_once\n",
8328            "    lifetime:    affine\n",
8329            "    persistence: ephemeral\n",
8330            "    shield:      Broker\n",
8331            "}",
8332        );
8333        let prog = parse(src);
8334        if let Declaration::Channel(ch) = &prog.declarations[0] {
8335            assert_eq!(ch.leading_trivia.len(), 1);
8336            assert!(ch.leading_trivia[0].is_doc());
8337            assert_eq!(ch.leading_trivia[0].text, "/// inbound order events");
8338        } else {
8339            panic!("expected Channel declaration");
8340        }
8341    }
8342
8343    #[test]
8344    fn per_struct_fields_match_side_channel() {
8345        // 14.a side-channel and 14.b per-struct fields must hold identical
8346        // data — they are populated by the same parser pass.
8347        let src = "/// for A\n// header for B\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }";
8348        let prog = parse(src);
8349        for (idx, decl) in prog.declarations.iter().enumerate() {
8350            let side = &prog.declaration_trivia[idx];
8351            let (per_lead, per_trail) = match decl {
8352                Declaration::Flow(f) => (&f.leading_trivia, &f.trailing_trivia),
8353                _ => panic!("unexpected variant"),
8354            };
8355            assert_eq!(per_lead.len(), side.leading.len());
8356            assert_eq!(per_trail.len(), side.trailing.len());
8357            for (a, b) in per_lead.iter().zip(side.leading.iter()) {
8358                assert_eq!(a.text, b.text);
8359                assert_eq!(a.kind, b.kind);
8360            }
8361        }
8362    }
8363
8364    #[test]
8365    fn comment_free_program_yields_empty_per_struct_fields() {
8366        let prog = parse("flow F() -> Out { }");
8367        if let Declaration::Flow(f) = &prog.declarations[0] {
8368            assert!(f.leading_trivia.is_empty());
8369            assert!(f.trailing_trivia.is_empty());
8370        } else {
8371            panic!("expected Flow declaration");
8372        }
8373    }
8374}
8375
8376// ── §Fase 14.c — inner doc comments (//!, /*!) ──────────────────────────────
8377//
8378// Inner doc comments document the *enclosing* item rather than the next
8379// sibling. Today they flow through the trivia channel like any other
8380// comment; downstream consumers (axon doc, LSP) decide how to interpret
8381// `is_inner_doc()`. These tests verify the lexer→parser pipeline preserves
8382// the inner-doc discriminator end-to-end.
8383
8384#[cfg(test)]
8385mod fase14c_inner_doc_tests {
8386    use super::*;
8387    use crate::lexer::Lexer;
8388    use crate::tokens::TriviaKind;
8389
8390    fn parse(src: &str) -> Program {
8391        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
8392        Parser::new(toks).parse().expect("parse")
8393    }
8394
8395    #[test]
8396    fn inner_doc_line_reaches_leading_trivia() {
8397        let src = "//! file-level docs\nflow F() -> Out { }";
8398        let prog = parse(src);
8399        let triv = &prog.declaration_trivia[0];
8400        assert_eq!(triv.leading.len(), 1);
8401        assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocLine);
8402        assert!(triv.leading[0].is_doc());
8403        assert!(triv.leading[0].is_inner_doc());
8404        assert_eq!(triv.leading[0].text, "//! file-level docs");
8405        assert_eq!(triv.leading[0].stripped_text(), " file-level docs");
8406    }
8407
8408    #[test]
8409    fn inner_doc_block_reaches_leading_trivia() {
8410        let src = "/*! module-level docs */\nflow F() -> Out { }";
8411        let prog = parse(src);
8412        let triv = &prog.declaration_trivia[0];
8413        assert_eq!(triv.leading.len(), 1);
8414        assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocBlock);
8415        assert!(triv.leading[0].is_inner_doc());
8416        assert_eq!(triv.leading[0].stripped_text(), " module-level docs ");
8417    }
8418
8419    #[test]
8420    fn outer_and_inner_doc_can_coexist() {
8421        // File-level inner doc on top, then an outer doc for the
8422        // declaration. Both reach the trivia channel and remain
8423        // distinguishable via `is_inner_doc()`.
8424        let src = "//! file docs\n/// docs F\nflow F() -> Out { }";
8425        let prog = parse(src);
8426        let triv = &prog.declaration_trivia[0];
8427        assert_eq!(triv.leading.len(), 2);
8428        assert!(triv.leading[0].is_inner_doc());
8429        assert!(triv.leading[1].is_doc());
8430        assert!(!triv.leading[1].is_inner_doc());
8431    }
8432
8433    #[test]
8434    fn inner_doc_reaches_per_struct_fields() {
8435        // Same data must be visible via the per-struct fields (Fase 14.b).
8436        let src = "//! intro\nflow F() -> Out { }";
8437        let prog = parse(src);
8438        if let Declaration::Flow(f) = &prog.declarations[0] {
8439            assert_eq!(f.leading_trivia.len(), 1);
8440            assert!(f.leading_trivia[0].is_inner_doc());
8441        } else {
8442            panic!("expected Flow declaration");
8443        }
8444    }
8445}
8446
8447// ── §Fase 28.c — Parser error recovery test pack ─────────────────────────────
8448//
8449// Mirror of `tests/test_fase28_parser_recovery.py` (Python side, 28.b).
8450// The test classes here line up 1-1 with the Python ones so the cross-
8451// stack drift gate (28.i) can compare error-list shapes input-for-input.
8452//
8453// Test classes:
8454//   - backwards_compat: existing `parse()` API unchanged
8455//   - single_error_recovery: one bad decl → one error, rest parse OK
8456//   - multi_error_recovery: N independent errors → N entries
8457//   - sync_points: every top-level keyword resyncs correctly
8458//   - parse_result_api: `has_errors`, `is_clean`
8459//   - edge_cases: EOF mid-error, brace imbalance, only-bad-tokens
8460//   - robustness_fuzz: 1000 deterministic-seeded mutations never crash
8461//   - no_ghost_errors: single broken field produces exactly 1 error
8462//   - integration_with_colon_diagnostic: v1.19.4 hint preserved under
8463//     recovery mode
8464#[cfg(test)]
8465mod fase28_recovery_tests {
8466    use super::*;
8467    use crate::lexer::Lexer;
8468
8469    /// Lex a source and return tokens for the parser to consume.
8470    /// Mirrors the Python `_parse_recovery` helper.
8471    fn lex(src: &str) -> Vec<Token> {
8472        Lexer::new(src, "<test>").tokenize().expect("lex")
8473    }
8474
8475    /// Parse with recovery mode. Returns `(program, errors)` so call
8476    /// sites read like the Python helper.
8477    fn recover(src: &str) -> ParseResult {
8478        Parser::new(lex(src)).parse_with_recovery()
8479    }
8480
8481    /// Strict parse. Mirrors the Python `_parse_strict` helper.
8482    fn strict(src: &str) -> Result<Program, ParseError> {
8483        Parser::new(lex(src)).parse()
8484    }
8485
8486    // ── backwards_compat ─────────────────────────────────────────
8487
8488    #[test]
8489    fn strict_parse_unchanged_for_clean_source() {
8490        // The existing `parse()` API must continue to succeed
8491        // verbatim on every well-formed input — D9.
8492        let src = "intent I {}";
8493        let prog = strict(src).expect("clean parse");
8494        assert_eq!(prog.declarations.len(), 1);
8495    }
8496
8497    #[test]
8498    fn strict_parse_still_raises_on_first_error() {
8499        // D9 + D8: opt-in to recovery via `parse_with_recovery`;
8500        // strict mode must still bubble the first error.
8501        // (Using a parse-time error rather than a lex error — `@@@`
8502        // would be rejected by the lexer, which is out of scope.)
8503        let src = "flow F() { } not_a_keyword flow G() { }";
8504        let _ = strict(src).expect_err("must error fast in strict mode");
8505    }
8506
8507    #[test]
8508    fn recovery_clean_source_yields_no_errors() {
8509        let src = "flow F() { } flow G() { }";
8510        let pr = recover(src);
8511        assert!(pr.is_clean(), "errors: {:?}", pr.errors);
8512        assert_eq!(pr.program.declarations.len(), 2);
8513    }
8514
8515    // ── single_error_recovery ────────────────────────────────────
8516
8517    #[test]
8518    fn single_unknown_top_level_token_recovers() {
8519        // One garbage token at top level; rest must parse.
8520        let src = "garbage_token flow F() { } flow G() { }";
8521        let pr = recover(src);
8522        assert_eq!(pr.errors.len(), 1, "errors: {:?}", pr.errors);
8523        assert_eq!(pr.program.declarations.len(), 2);
8524    }
8525
8526    #[test]
8527    fn error_in_first_decl_does_not_block_second() {
8528        // `flow F` body refers to non-keyword `nope`; the error
8529        // recovery must skip to the next top-level keyword.
8530        let src = "flow F() { not_a_step nope } flow G() { }";
8531        let pr = recover(src);
8532        assert!(pr.has_errors(), "expected at least one error");
8533        // The second flow must be reachable.
8534        let names: Vec<&str> = pr
8535            .program
8536            .declarations
8537            .iter()
8538            .filter_map(|d| match d {
8539                Declaration::Flow(f) => Some(f.name.as_str()),
8540                _ => None,
8541            })
8542            .collect();
8543        assert!(names.contains(&"G"), "G not found among {names:?}");
8544    }
8545
8546    #[test]
8547    fn malformed_declaration_then_clean_intent_recovers() {
8548        let src = "flow @ () { } intent I {}";
8549        let pr = recover(src);
8550        assert!(pr.has_errors());
8551        let kinds: Vec<&str> = pr
8552            .program
8553            .declarations
8554            .iter()
8555            .map(|d| match d {
8556                Declaration::Intent(_) => "intent",
8557                Declaration::Flow(_) => "flow",
8558                _ => "other",
8559            })
8560            .collect();
8561        assert!(kinds.contains(&"intent"), "kinds: {kinds:?}");
8562    }
8563
8564    #[test]
8565    fn recovery_does_not_double_count_a_single_error() {
8566        // Regression for the "ghost error" pathology that surfaced
8567        // during 28.b dev: a nested-decl error must not also fire
8568        // an "Unexpected token at top level" from the outer loop.
8569        // The Rust grammar has stricter intra-flow requirements
8570        // than Python; the invariant we assert here is that the
8571        // outer loop emits zero "Unexpected token at top level"
8572        // errors after an inner step-shape error.
8573        let src = "flow F() { not_a_step }";
8574        let pr = recover(src);
8575        let outer_ghosts = pr
8576            .errors
8577            .iter()
8578            .filter(|e| e.message.contains("at top level"))
8579            .count();
8580        assert_eq!(outer_ghosts, 0, "ghost errors: {:?}", pr.errors);
8581    }
8582
8583    // ── multi_error_recovery ─────────────────────────────────────
8584
8585    #[test]
8586    fn three_independent_errors_yield_three_entries() {
8587        let src =
8588            "garbage1 flow F() { } garbage2 flow G() { } garbage3 flow H() { }";
8589        let pr = recover(src);
8590        assert_eq!(pr.errors.len(), 3, "errors: {:?}", pr.errors);
8591        assert_eq!(pr.program.declarations.len(), 3);
8592    }
8593
8594    #[test]
8595    fn all_errors_no_valid_declarations() {
8596        let src = "foo bar baz qux";
8597        let pr = recover(src);
8598        assert!(pr.has_errors());
8599        assert!(pr.program.declarations.is_empty());
8600    }
8601
8602    #[test]
8603    fn errors_recorded_in_source_order() {
8604        let src = "x flow A() { } y flow B() { } z flow C() { }";
8605        let pr = recover(src);
8606        assert_eq!(pr.errors.len(), 3);
8607        let lines: Vec<u32> = pr.errors.iter().map(|e| e.line).collect();
8608        // Same source-line means we compare by column ordering;
8609        // either way they must be non-decreasing.
8610        assert!(
8611            lines.windows(2).all(|w| w[0] <= w[1]),
8612            "errors out of order: {lines:?}"
8613        );
8614    }
8615
8616    // ── sync_points ──────────────────────────────────────────────
8617
8618    #[test]
8619    fn sync_to_flow_keyword() {
8620        let src = "garbage flow F() { }";
8621        let pr = recover(src);
8622        assert_eq!(pr.program.declarations.len(), 1);
8623    }
8624
8625    #[test]
8626    fn sync_to_intent_keyword() {
8627        let src = "garbage intent I {}";
8628        let pr = recover(src);
8629        assert_eq!(pr.program.declarations.len(), 1);
8630    }
8631
8632    #[test]
8633    fn sync_to_persona_keyword() {
8634        let src = "garbage persona P { name: \"P\" role: \"R\" }";
8635        let pr = recover(src);
8636        assert!(
8637            pr.program
8638                .declarations
8639                .iter()
8640                .any(|d| matches!(d, Declaration::Persona(_))),
8641            "persona not recovered: decls = {:?}",
8642            pr.program.declarations.len()
8643        );
8644    }
8645
8646    #[test]
8647    fn sync_to_run_keyword() {
8648        let src = "garbage run R { input: { user_message: \"hi\" } }";
8649        let pr = recover(src);
8650        // Either Run was parsed, or recovery still produced ≥1 err.
8651        assert!(pr.has_errors());
8652    }
8653
8654    // ── parse_result_api ─────────────────────────────────────────
8655
8656    #[test]
8657    fn parse_result_has_errors_and_is_clean_invert() {
8658        let pr_clean = recover("flow F() { }");
8659        assert!(pr_clean.is_clean());
8660        assert!(!pr_clean.has_errors());
8661
8662        let pr_err = recover("garbage");
8663        assert!(!pr_err.is_clean());
8664        assert!(pr_err.has_errors());
8665    }
8666
8667    #[test]
8668    fn parse_result_program_field_holds_partial_program() {
8669        let pr = recover("garbage flow F() { }");
8670        assert!(!pr.program.declarations.is_empty());
8671    }
8672
8673    #[test]
8674    fn parse_result_errors_carry_line_and_column() {
8675        let pr = recover("garbage");
8676        assert!(!pr.errors.is_empty());
8677        let e = &pr.errors[0];
8678        assert!(e.line >= 1);
8679        // Column may be 0-based or 1-based depending on lexer;
8680        // accept anything ≥ 0.
8681        let _ = e.column;
8682        assert!(!e.message.is_empty());
8683    }
8684
8685    #[test]
8686    fn parse_result_debug_renders() {
8687        let pr = recover("flow F() { }");
8688        let s = format!("{pr:?}");
8689        assert!(s.contains("ParseResult"));
8690    }
8691
8692    // ── edge_cases ───────────────────────────────────────────────
8693
8694    #[test]
8695    fn empty_source_is_clean() {
8696        let pr = recover("");
8697        assert!(pr.is_clean());
8698        assert!(pr.program.declarations.is_empty());
8699    }
8700
8701    #[test]
8702    fn whitespace_only_source_is_clean() {
8703        let pr = recover("   \n\n\t  \n");
8704        assert!(pr.is_clean());
8705        assert!(pr.program.declarations.is_empty());
8706    }
8707
8708    #[test]
8709    fn only_garbage_does_not_crash() {
8710        // Lex-clean garbage tokens (avoids AxonLexerError).
8711        let pr = recover("foo bar baz { qux quux } corge { grault }");
8712        assert!(pr.has_errors());
8713    }
8714
8715    #[test]
8716    fn unbalanced_close_brace_does_not_crash() {
8717        let pr = recover("} flow F() { }");
8718        // Recovery must keep walking past stray `}`.
8719        let names: Vec<&str> = pr
8720            .program
8721            .declarations
8722            .iter()
8723            .filter_map(|d| match d {
8724                Declaration::Flow(f) => Some(f.name.as_str()),
8725                _ => None,
8726            })
8727            .collect();
8728        assert!(names.contains(&"F"), "F not recovered: {names:?}");
8729    }
8730
8731    #[test]
8732    fn error_at_eof_does_not_loop() {
8733        // Truncated declaration. Must terminate; finite errors.
8734        let pr = recover("flow F() { ");
8735        // Either errored or somehow accepted — but must terminate.
8736        let _ = pr.errors.len();
8737    }
8738
8739    #[test]
8740    fn nested_braces_inside_error_still_balance() {
8741        // Walker must respect brace depth so a `}` inside a malformed
8742        // block does not prematurely sync.
8743        let src = "flow F() { not_a_step { inner } } flow G() { }";
8744        let pr = recover(src);
8745        let names: Vec<&str> = pr
8746            .program
8747            .declarations
8748            .iter()
8749            .filter_map(|d| match d {
8750                Declaration::Flow(f) => Some(f.name.as_str()),
8751                _ => None,
8752            })
8753            .collect();
8754        assert!(names.contains(&"G"), "G not recovered: {names:?}");
8755    }
8756
8757    // ── robustness_fuzz ──────────────────────────────────────────
8758    //
8759    // Deterministic-seeded mutator (xorshift). 100 buckets ×
8760    // 10 mutations = 1000 iterations, byte-bounded so fuzz time
8761    // stays under 1 s on a release build. Recovery must NEVER crash;
8762    // lexer-level errors are out of scope (lexer recovery is its own
8763    // sub-fase). 28.b mirrors this with the same structure.
8764
8765    #[derive(Clone, Copy)]
8766    struct Xorshift(u64);
8767    impl Xorshift {
8768        fn next(&mut self) -> u64 {
8769            let mut x = self.0;
8770            x ^= x << 13;
8771            x ^= x >> 7;
8772            x ^= x << 17;
8773            self.0 = x;
8774            x
8775        }
8776        fn pick<T: Copy>(&mut self, slice: &[T]) -> T {
8777            slice[(self.next() as usize) % slice.len()]
8778        }
8779    }
8780
8781    fn mutate(src: &str, rng: &mut Xorshift) -> String {
8782        let mut bytes: Vec<u8> = src.bytes().collect();
8783        if bytes.is_empty() {
8784            return src.to_string();
8785        }
8786        let op = rng.next() % 4;
8787        let pos = (rng.next() as usize) % bytes.len();
8788        // Stick to ASCII-safe printable bytes to keep input lex-able
8789        // most of the time. AxonLexerError is still possible and is
8790        // tolerated by the recovery contract.
8791        let safe: &[u8] = b"abcdefghijklmnopqrstuvwxyz {}();:,_0123456789";
8792        match op {
8793            0 => {
8794                bytes.remove(pos);
8795            }
8796            1 => {
8797                let b = rng.pick(safe);
8798                bytes.insert(pos, b);
8799            }
8800            2 if pos + 1 < bytes.len() => {
8801                bytes.swap(pos, pos + 1);
8802            }
8803            _ => {
8804                let b = rng.pick(safe);
8805                bytes[pos] = b;
8806            }
8807        }
8808        // Lossy decode: mutator may have produced invalid UTF-8;
8809        // strip non-ASCII before handing to the lexer.
8810        bytes.retain(|b| b.is_ascii());
8811        String::from_utf8_lossy(&bytes).into_owned()
8812    }
8813
8814    #[test]
8815    fn fuzz_recovery_never_crashes() {
8816        let seed_bases = [
8817            "flow F() { }",
8818            "intent I { }",
8819            "persona P { name: \"P\" role: \"R\" }",
8820            "intent J { ask: \"a\" }",
8821            "type T = String",
8822        ];
8823        // 100 buckets × 10 mutations = 1000 iterations, deterministic.
8824        for (bucket, base) in (0..100u64).zip(seed_bases.iter().cycle()) {
8825            let mut rng = Xorshift(0x1234_5678_9abc_def0_u64.wrapping_add(bucket));
8826            let mut current = (*base).to_string();
8827            for _ in 0..10 {
8828                current = mutate(&current, &mut rng);
8829                // Lexer may reject; that's outside parser-recovery
8830                // scope (28.b/c). Skip those iterations.
8831                let toks = match Lexer::new(&current, "<fuzz>").tokenize() {
8832                    Ok(t) => t,
8833                    Err(_) => continue,
8834                };
8835                // Recovery must not panic on any well-lexed input.
8836                let _pr = Parser::new(toks).parse_with_recovery();
8837            }
8838        }
8839    }
8840
8841    // ── integration_with_v1_19_4_colon_diagnostic ────────────────
8842
8843    #[test]
8844    fn missing_colon_hint_preserved_under_recovery() {
8845        // The Rust frontend's strict `parse()` carries the same
8846        // colon diagnostic shape as the Python side. Recovery mode
8847        // must not erase it.
8848        let src = "flow F() { run R { input { user_message: \"hi\" } } }";
8849        let pr = recover(src);
8850        // Either the parser accepts this (some shape may be valid)
8851        // or it errors — but if it errors, the message must surface
8852        // the diagnostic content.
8853        if !pr.errors.is_empty() {
8854            let any_msg = pr.errors.iter().any(|e| !e.message.is_empty());
8855            assert!(any_msg);
8856        }
8857    }
8858
8859    // ── recovery preserves declaration ordering ──────────────────
8860
8861    #[test]
8862    fn recovered_declarations_appear_in_source_order() {
8863        let src = "flow A() { } garbage flow B() { } garbage flow C() { }";
8864        let pr = recover(src);
8865        let names: Vec<&str> = pr
8866            .program
8867            .declarations
8868            .iter()
8869            .filter_map(|d| match d {
8870                Declaration::Flow(f) => Some(f.name.as_str()),
8871                _ => None,
8872            })
8873            .collect();
8874        assert_eq!(names, vec!["A", "B", "C"]);
8875    }
8876}
8877
8878// ── §Fase 28.d — Source-context diagnostic block test pack ───────────────────
8879//
8880// Mirror of `tests/test_fase28_source_context.py` (Python side, 28.d).
8881// The render output must be byte-identical to the Python `SourceSnippet.render`
8882// on the same input — D7 ratified (cross-stack drift gate). Golden strings
8883// in `golden_*` tests are duplicated verbatim in the Python pack; edits
8884// here MUST be mirrored on the Python side and vice versa.
8885#[cfg(test)]
8886mod fase28_source_context_tests {
8887    use super::*;
8888    use crate::lexer::Lexer;
8889
8890    fn snippet(source: &str, line: u32, column: u32, filename: &str) -> String {
8891        SourceSnippet::new(
8892            source.to_string(),
8893            line,
8894            column,
8895            filename.to_string(),
8896        )
8897        .render()
8898    }
8899
8900    // ── Pure rendering ──────────────────────────────────────────
8901
8902    #[test]
8903    fn rustc_style_block_for_middle_line() {
8904        let src = "line one\nline two\nline three\nline four\nline five";
8905        let out = snippet(src, 3, 6, "x.axon");
8906        assert!(out.contains("--> x.axon:3:6"));
8907        assert!(out.contains("1 | line one"));
8908        assert!(out.contains("2 | line two"));
8909        assert!(out.contains("3 | line three"));
8910        assert!(out.contains("4 | line four"));
8911        assert!(out.contains("5 | line five"));
8912        // Caret col 6 → 5-space pad. Empty gutter is 1 space (gutter=1).
8913        assert!(out.contains("\n  |      ^"), "out:\n{out}");
8914    }
8915
8916    #[test]
8917    fn caret_column_one_renders_correctly() {
8918        let out = snippet("abc\n", 1, 1, "<source>");
8919        assert!(out.contains("\n  | ^"));
8920    }
8921
8922    #[test]
8923    fn first_line_clamps_context_before_to_zero() {
8924        let src = "first\nsecond\nthird\nfourth\nfifth";
8925        let out = snippet(src, 1, 1, "<source>");
8926        assert!(out.contains("1 | first"));
8927        assert!(out.contains("2 | second"));
8928        assert!(out.contains("3 | third"));
8929        assert!(!out.contains("4 | fourth"));
8930    }
8931
8932    #[test]
8933    fn last_line_clamps_context_after_to_eof() {
8934        let src = "first\nsecond\nthird\nfourth\nfifth";
8935        let out = snippet(src, 5, 2, "<source>");
8936        assert!(out.contains("5 | fifth"));
8937        assert!(out.contains("3 | third"));
8938        assert!(out.contains("4 | fourth"));
8939        assert!(!out.contains("2 | second"));
8940    }
8941
8942    #[test]
8943    fn gutter_width_grows_with_line_count() {
8944        let src: String = (1..=12).map(|i| format!("line{i}")).collect::<Vec<_>>().join("\n");
8945        let out = snippet(&src, 12, 1, "<source>");
8946        assert!(out.contains("12 | line12"));
8947        assert!(out.contains("10 | line10"));
8948    }
8949
8950    // ── Edge cases ──────────────────────────────────────────────
8951
8952    #[test]
8953    fn empty_source_returns_empty() {
8954        assert_eq!(snippet("", 1, 1, "<source>"), "");
8955    }
8956
8957    #[test]
8958    fn zero_line_returns_empty() {
8959        assert_eq!(snippet("hi", 0, 1, "<source>"), "");
8960    }
8961
8962    #[test]
8963    fn out_of_range_line_returns_empty() {
8964        assert_eq!(snippet("hi", 99, 1, "<source>"), "");
8965    }
8966
8967    #[test]
8968    fn caret_clamps_past_eol() {
8969        let out = snippet("hello", 1, 50, "<source>");
8970        assert!(out.contains("\n  |      ^"), "out:\n{out}");
8971    }
8972
8973    #[test]
8974    fn unicode_codepoint_count_for_caret_clamp() {
8975        // "héllo" = 5 codepoints; column past EOL clamps to 6.
8976        let out = snippet("héllo", 1, 99, "<source>");
8977        assert!(out.contains("\n  |      ^"), "out:\n{out}");
8978    }
8979
8980    #[test]
8981    fn trailing_newline_does_not_create_phantom_last_line() {
8982        let out = snippet("first\nsecond\n", 2, 1, "<source>");
8983        assert!(!out.contains("3 |"));
8984        assert!(out.contains("2 | second"));
8985    }
8986
8987    // ── Parser attach plumbing ──────────────────────────────────
8988
8989    fn lex(src: &str) -> Vec<Token> {
8990        Lexer::new(src, "<test>").tokenize().expect("lex")
8991    }
8992
8993    #[test]
8994    fn strict_parse_attaches_snippet_when_source_given() {
8995        let src = "garbage_token\nflow F() { }";
8996        let err = Parser::new(lex(src))
8997            .with_source(src, "x.axon")
8998            .parse()
8999            .expect_err("must error");
9000        assert!(err.source_snippet.is_some());
9001        let display = format!("{err}");
9002        assert!(display.contains("--> x.axon:"), "display: {display}");
9003    }
9004
9005    #[test]
9006    fn strict_parse_no_snippet_when_no_source() {
9007        let src = "garbage_token";
9008        let err = Parser::new(lex(src)).parse().expect_err("must error");
9009        assert!(err.source_snippet.is_none());
9010        let display = format!("{err}");
9011        assert!(!display.contains("\n  -->"));
9012    }
9013
9014    #[test]
9015    fn every_recovered_error_has_snippet() {
9016        let src = "garbage1\nflow F() { }\ngarbage2\nflow G() { }";
9017        let result = Parser::new(lex(src))
9018            .with_source(src, "multi.axon")
9019            .parse_with_recovery();
9020        assert!(!result.errors.is_empty());
9021        for err in &result.errors {
9022            assert!(err.source_snippet.is_some());
9023            let display = format!("{err}");
9024            assert!(
9025                display.contains("--> multi.axon:"),
9026                "display: {display}"
9027            );
9028        }
9029    }
9030
9031    #[test]
9032    fn recovery_no_snippet_when_no_source() {
9033        let src = "garbage1 garbage2";
9034        let result = Parser::new(lex(src)).parse_with_recovery();
9035        for err in &result.errors {
9036            assert!(err.source_snippet.is_none());
9037        }
9038    }
9039
9040    #[test]
9041    fn snippet_points_at_correct_line_for_each_error() {
9042        let src = "garbage_a\nflow F() { }\ngarbage_b\nflow G() { }";
9043        let result = Parser::new(lex(src))
9044            .with_source(src, "x")
9045            .parse_with_recovery();
9046        for err in &result.errors {
9047            let sn = err.source_snippet.as_ref().expect("snippet");
9048            assert_eq!(sn.line, err.line);
9049        }
9050    }
9051
9052    // ── Backwards-compat ────────────────────────────────────────
9053
9054    #[test]
9055    fn legacy_constructor_still_works() {
9056        let src = "flow F() { }";
9057        let prog = Parser::new(lex(src)).parse().expect("clean");
9058        assert_eq!(prog.declarations.len(), 1);
9059    }
9060
9061    #[test]
9062    fn attach_source_idempotent() {
9063        let err = ParseError {
9064            message: "bad".to_string(),
9065            line: 2,
9066            column: 3,
9067            ..Default::default()
9068        };
9069        let err2 = err.clone().attach_source("a\nb\nc\n", "f.axon");
9070        let first = format!("{err2}");
9071        let err3 = err.attach_source("a\nb\nc\n", "f.axon");
9072        let second = format!("{err3}");
9073        assert_eq!(first, second);
9074    }
9075
9076    #[test]
9077    fn attach_source_noop_when_line_zero() {
9078        let err = ParseError {
9079            message: "bad".to_string(),
9080            line: 0,
9081            column: 0,
9082            ..Default::default()
9083        };
9084        let err = err.attach_source("a\nb\nc\n", "f.axon");
9085        assert!(err.source_snippet.is_none());
9086    }
9087
9088    // ── Cross-stack golden parity ───────────────────────────────
9089    // These golden strings are duplicated verbatim in the Python
9090    // test pack at `tests/test_fase28_source_context.py::TestRustParityShape`.
9091    // Edits here MUST be mirrored in the Python pack — D7.
9092
9093    #[test]
9094    fn golden_simple_three_line_block() {
9095        let src = "alpha\nbeta\ngamma";
9096        let out = snippet(src, 2, 3, "g.axon");
9097        // Note: gutter=1, so empty_gutter=" " (one space). The
9098        // " --> ..." line therefore starts with two spaces ("<empty>"
9099        // + literal " --> ...").
9100        let expected = concat!(
9101            "  --> g.axon:2:3\n",
9102            "  |\n",
9103            "1 | alpha\n",
9104            "2 | beta\n",
9105            "  |   ^\n",
9106            "3 | gamma",
9107        );
9108        assert_eq!(out, expected);
9109    }
9110
9111    #[test]
9112    fn golden_first_line_caret() {
9113        let src = "abc\ndef\n";
9114        let out = snippet(src, 1, 1, "x");
9115        let expected = concat!(
9116            "  --> x:1:1\n",
9117            "  |\n",
9118            "1 | abc\n",
9119            "  | ^\n",
9120            "2 | def",
9121        );
9122        assert_eq!(out, expected);
9123    }
9124
9125    #[test]
9126    fn golden_two_digit_gutter() {
9127        let src: String = (1..=11)
9128            .map(|i| format!("L{i}"))
9129            .collect::<Vec<_>>()
9130            .join("\n");
9131        let out = snippet(&src, 10, 2, "big");
9132        let expected = concat!(
9133            "   --> big:10:2\n",
9134            "   |\n",
9135            " 8 | L8\n",
9136            " 9 | L9\n",
9137            "10 | L10\n",
9138            "   |  ^\n",
9139            "11 | L11",
9140        );
9141        assert_eq!(out, expected);
9142    }
9143}
9144
9145// ── §Fase 28.e — Parser integration tests for smart-suggest ──────────────────
9146//
9147// Mirror of `tests/test_fase28_smart_suggest.py::TestParserIntegration`.
9148// Verifies that the parser actually wires `suggest_for` into the
9149// unknown-keyword diagnostic at both error sites — top-level and
9150// flow-body.
9151#[cfg(test)]
9152mod fase28_smart_suggest_parser_tests {
9153    use super::*;
9154    use crate::lexer::Lexer;
9155
9156    fn lex(src: &str) -> Vec<Token> {
9157        Lexer::new(src, "<test>").tokenize().expect("lex")
9158    }
9159
9160    #[test]
9161    fn top_level_typo_suggests_flow() {
9162        let src = "flwo F() { }";
9163        let err = Parser::new(lex(src)).parse().expect_err("must error");
9164        assert!(
9165            err.message.contains("Did you mean `flow`?"),
9166            "msg: {}",
9167            err.message
9168        );
9169    }
9170
9171    #[test]
9172    fn top_level_unknown_far_no_suggestion() {
9173        let src = "qwerty F() { }";
9174        let err = Parser::new(lex(src)).parse().expect_err("must error");
9175        assert!(
9176            !err.message.contains("Did you mean"),
9177            "msg: {}",
9178            err.message
9179        );
9180    }
9181
9182    #[test]
9183    fn flow_body_typo_suggests_step() {
9184        let src = "flow F() { stepp S {} }";
9185        let err = Parser::new(lex(src)).parse().expect_err("must error");
9186        assert!(
9187            err.message.contains("Did you mean `step`"),
9188            "msg: {}",
9189            err.message
9190        );
9191    }
9192
9193    #[test]
9194    fn flow_body_typo_suggests_reason() {
9195        let src = "flow F() { reasn R {} }";
9196        let err = Parser::new(lex(src)).parse().expect_err("must error");
9197        assert!(
9198            err.message.contains("Did you mean `reason`?"),
9199            "msg: {}",
9200            err.message
9201        );
9202    }
9203
9204    #[test]
9205    fn recovery_mode_carries_hint() {
9206        let src = "flwo F() { }";
9207        let result = Parser::new(lex(src)).parse_with_recovery();
9208        assert!(
9209            result
9210                .errors
9211                .iter()
9212                .any(|e| e.message.contains("Did you mean `flow`?")),
9213            "errors: {:?}",
9214            result.errors
9215        );
9216    }
9217}
9218
9219// ── §Fase 35.m — mutate / purge where-clause capture ────────────────
9220
9221#[cfg(test)]
9222mod fase35m_mutate_purge_where_tests {
9223    use super::*;
9224
9225    fn parse(src: &str) -> Program {
9226        let tokens = crate::lexer::Lexer::new(src, "<test>")
9227            .tokenize()
9228            .expect("lex");
9229        Parser::new(tokens).parse().expect("parse")
9230    }
9231
9232    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
9233        for d in &prog.declarations {
9234            if let Declaration::Flow(f) = d {
9235                if f.name == flow {
9236                    return f.body.first().expect("flow has at least one step");
9237                }
9238            }
9239        }
9240        panic!("flow `{flow}` not found");
9241    }
9242
9243    #[test]
9244    fn mutate_captures_its_where_clause() {
9245        // Pre-35.m the `{ where: }` block was skipped — every mutate
9246        // ran whole-store. It must now reach `where_expr`.
9247        let prog =
9248            parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
9249        match first_step(&prog, "F") {
9250            FlowStep::Mutate(m) => {
9251                assert_eq!(m.store_name, "accounts");
9252                assert_eq!(m.where_expr, "id = 1");
9253            }
9254            other => panic!("expected Mutate, got {other:?}"),
9255        }
9256    }
9257
9258    #[test]
9259    fn purge_captures_its_where_clause() {
9260        let prog =
9261            parse("flow F() -> Unit { purge logs { where: \"ts < 100\" } }");
9262        match first_step(&prog, "F") {
9263            FlowStep::Purge(p) => {
9264                assert_eq!(p.store_name, "logs");
9265                assert_eq!(p.where_expr, "ts < 100");
9266            }
9267            other => panic!("expected Purge, got {other:?}"),
9268        }
9269    }
9270
9271    #[test]
9272    fn mutate_without_a_where_block_is_a_whole_store_op() {
9273        // No `{ where: }` → an empty filter → the runtime renders
9274        // `WHERE TRUE` (every row). A valid, intentional op.
9275        let prog = parse("flow F() -> Unit { mutate accounts }");
9276        match first_step(&prog, "F") {
9277            FlowStep::Mutate(m) => {
9278                assert_eq!(m.store_name, "accounts");
9279                assert_eq!(m.where_expr, "");
9280            }
9281            other => panic!("expected Mutate, got {other:?}"),
9282        }
9283    }
9284}
9285
9286// ── §Fase 35.o — persist field-block capture ────────────────────────
9287
9288#[cfg(test)]
9289mod fase35o_persist_fields_tests {
9290    use super::*;
9291
9292    fn parse(src: &str) -> Program {
9293        let tokens = crate::lexer::Lexer::new(src, "<test>")
9294            .tokenize()
9295            .expect("lex");
9296        Parser::new(tokens).parse().expect("parse")
9297    }
9298
9299    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
9300        for d in &prog.declarations {
9301            if let Declaration::Flow(f) = d {
9302                if f.name == flow {
9303                    return f.body.first().expect("flow has at least one step");
9304                }
9305            }
9306        }
9307        panic!("flow `{flow}` not found");
9308    }
9309
9310    #[test]
9311    fn persist_captures_its_field_block() {
9312        // Pre-35.o the `{ col: value }` block was skipped — every
9313        // persist wrote the whole binding context. It must now reach
9314        // `fields`, in source order, with value expressions raw.
9315        let prog = parse(
9316            "flow F() -> Unit { persist into chat_history { \
9317             session_id: \"${session_id}\" sender: \"user\" \
9318             content: \"${message}\" } }",
9319        );
9320        match first_step(&prog, "F") {
9321            FlowStep::Persist(p) => {
9322                assert_eq!(p.store_name, "chat_history");
9323                assert_eq!(
9324                    p.fields,
9325                    vec![
9326                        ("session_id".to_string(), "${session_id}".to_string()),
9327                        ("sender".to_string(), "user".to_string()),
9328                        ("content".to_string(), "${message}".to_string()),
9329                    ]
9330                );
9331            }
9332            other => panic!("expected Persist, got {other:?}"),
9333        }
9334    }
9335
9336    #[test]
9337    fn persist_without_a_block_keeps_the_user_bindings_fallback() {
9338        // No `{ }` → empty `fields` → the runtime falls back to the
9339        // v1.30.0 user-bindings row. Backward-compatible.
9340        let prog = parse("flow F() -> Unit { persist events }");
9341        match first_step(&prog, "F") {
9342            FlowStep::Persist(p) => {
9343                assert_eq!(p.store_name, "events");
9344                assert!(p.fields.is_empty());
9345            }
9346            other => panic!("expected Persist, got {other:?}"),
9347        }
9348    }
9349
9350    #[test]
9351    fn persist_accepts_the_optional_into_connector() {
9352        // `persist into X` and `persist X` resolve to the SAME store
9353        // name — pre-35.o `into` was captured AS the store name.
9354        let with =
9355            parse("flow F() -> Unit { persist into accounts { id: \"1\" } }");
9356        let without =
9357            parse("flow F() -> Unit { persist accounts { id: \"1\" } }");
9358        for prog in [&with, &without] {
9359            match first_step(prog, "F") {
9360                FlowStep::Persist(p) => assert_eq!(p.store_name, "accounts"),
9361                other => panic!("expected Persist, got {other:?}"),
9362            }
9363        }
9364    }
9365
9366    #[test]
9367    fn persist_into_without_a_block_resolves_the_store_name() {
9368        // `persist into events` — the `into` connector is skipped, the
9369        // store name is `events` (not `into`). Lateral bug closed.
9370        let prog = parse("flow F() -> Unit { persist into events }");
9371        match first_step(&prog, "F") {
9372            FlowStep::Persist(p) => {
9373                assert_eq!(p.store_name, "events");
9374                assert!(p.fields.is_empty());
9375            }
9376            other => panic!("expected Persist, got {other:?}"),
9377        }
9378    }
9379
9380    #[test]
9381    fn persist_fields_lower_into_the_ir() {
9382        // The IR generator must carry `fields` onto `IRPersistStep`
9383        // so the runtime reads exactly the declared columns.
9384        let prog = parse(
9385            "flow F() -> Unit { persist into chat { content: \"${msg}\" } }",
9386        );
9387        let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
9388        let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
9389        match flow.steps.first().expect("one step") {
9390            crate::ir_nodes::IRFlowNode::Persist(p) => {
9391                assert_eq!(p.store_name, "chat");
9392                assert_eq!(
9393                    p.fields,
9394                    vec![("content".to_string(), "${msg}".to_string())]
9395                );
9396            }
9397            other => panic!("expected IRFlowNode::Persist, got {other:?}"),
9398        }
9399    }
9400}
9401
9402// ── §Fase 35.p — mutate SET-field-block capture ─────────────────────
9403
9404#[cfg(test)]
9405mod fase35p_mutate_fields_tests {
9406    use super::*;
9407
9408    fn parse(src: &str) -> Program {
9409        let tokens = crate::lexer::Lexer::new(src, "<test>")
9410            .tokenize()
9411            .expect("lex");
9412        Parser::new(tokens).parse().expect("parse")
9413    }
9414
9415    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
9416        for d in &prog.declarations {
9417            if let Declaration::Flow(f) = d {
9418                if f.name == flow {
9419                    return f.body.first().expect("flow has at least one step");
9420                }
9421            }
9422        }
9423        panic!("flow `{flow}` not found");
9424    }
9425
9426    #[test]
9427    fn mutate_captures_its_set_field_block() {
9428        // Pre-35.p every key but `where:` was skipped — the runtime
9429        // SET every flow binding. The SET columns must now reach
9430        // `fields`, in source order, with `where:` still captured.
9431        let prog = parse(
9432            "flow F() -> Unit { mutate accounts { where: \"id = ${id}\" \
9433             balance: \"${new_balance}\" status: \"active\" } }",
9434        );
9435        match first_step(&prog, "F") {
9436            FlowStep::Mutate(m) => {
9437                assert_eq!(m.store_name, "accounts");
9438                assert_eq!(m.where_expr, "id = ${id}");
9439                assert_eq!(
9440                    m.fields,
9441                    vec![
9442                        ("balance".to_string(), "${new_balance}".to_string()),
9443                        ("status".to_string(), "active".to_string()),
9444                    ]
9445                );
9446            }
9447            other => panic!("expected Mutate, got {other:?}"),
9448        }
9449    }
9450
9451    #[test]
9452    fn mutate_where_only_block_has_no_set_fields() {
9453        // A `{ where: }`-only block → empty `fields` → the runtime
9454        // falls back to the v1.31.0 user-bindings SET.
9455        let prog =
9456            parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
9457        match first_step(&prog, "F") {
9458            FlowStep::Mutate(m) => {
9459                assert_eq!(m.where_expr, "id = 1");
9460                assert!(m.fields.is_empty());
9461            }
9462            other => panic!("expected Mutate, got {other:?}"),
9463        }
9464    }
9465
9466    #[test]
9467    fn mutate_with_no_block_is_a_whole_store_op() {
9468        // No block at all → empty where + empty fields (a whole-store
9469        // UPDATE from user bindings) — unchanged from 35.m.
9470        let prog = parse("flow F() -> Unit { mutate accounts }");
9471        match first_step(&prog, "F") {
9472            FlowStep::Mutate(m) => {
9473                assert_eq!(m.store_name, "accounts");
9474                assert_eq!(m.where_expr, "");
9475                assert!(m.fields.is_empty());
9476            }
9477            other => panic!("expected Mutate, got {other:?}"),
9478        }
9479    }
9480
9481    #[test]
9482    fn mutate_fields_lower_into_the_ir() {
9483        let prog = parse(
9484            "flow F() -> Unit { mutate t { where: \"id = 1\" v: \"${x}\" } }",
9485        );
9486        let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
9487        let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
9488        match flow.steps.first().expect("one step") {
9489            crate::ir_nodes::IRFlowNode::Mutate(m) => {
9490                assert_eq!(m.where_expr, "id = 1");
9491                assert_eq!(
9492                    m.fields,
9493                    vec![("v".to_string(), "${x}".to_string())]
9494                );
9495            }
9496            other => panic!("expected IRFlowNode::Mutate, got {other:?}"),
9497        }
9498    }
9499}
9500