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::Window(n) => {
114            n.leading_trivia = leading;
115            n.trailing_trivia = trailing;
116        }
117        Declaration::Pix(n) => {
118            n.leading_trivia = leading;
119            n.trailing_trivia = trailing;
120        }
121        Declaration::Ledger(n) => {
122            n.leading_trivia = leading;
123            n.trailing_trivia = trailing;
124        }
125        Declaration::Psyche(n) => {
126            n.leading_trivia = leading;
127            n.trailing_trivia = trailing;
128        }
129        Declaration::Corpus(n) => {
130            n.leading_trivia = leading;
131            n.trailing_trivia = trailing;
132        }
133        Declaration::Dataspace(n) => {
134            n.leading_trivia = leading;
135            n.trailing_trivia = trailing;
136        }
137        Declaration::Ots(n) => {
138            n.leading_trivia = leading;
139            n.trailing_trivia = trailing;
140        }
141        Declaration::Mandate(n) => {
142            n.leading_trivia = leading;
143            n.trailing_trivia = trailing;
144        }
145        Declaration::Compute(n) => {
146            n.leading_trivia = leading;
147            n.trailing_trivia = trailing;
148        }
149        Declaration::Daemon(n) => {
150            n.leading_trivia = leading;
151            n.trailing_trivia = trailing;
152        }
153        Declaration::Extension(n) => {
154            n.leading_trivia = leading;
155            n.trailing_trivia = trailing;
156        }
157        Declaration::AxonStore(n) => {
158            n.leading_trivia = leading;
159            n.trailing_trivia = trailing;
160        }
161        Declaration::AxonEndpoint(n) => {
162            n.leading_trivia = leading;
163            n.trailing_trivia = trailing;
164        }
165        Declaration::Resource(n) => {
166            n.leading_trivia = leading;
167            n.trailing_trivia = trailing;
168        }
169        Declaration::Fabric(n) => {
170            n.leading_trivia = leading;
171            n.trailing_trivia = trailing;
172        }
173        Declaration::Manifest(n) => {
174            n.leading_trivia = leading;
175            n.trailing_trivia = trailing;
176        }
177        Declaration::Observe(n) => {
178            n.leading_trivia = leading;
179            n.trailing_trivia = trailing;
180        }
181        Declaration::Reconcile(n) => {
182            n.leading_trivia = leading;
183            n.trailing_trivia = trailing;
184        }
185        Declaration::Lease(n) => {
186            n.leading_trivia = leading;
187            n.trailing_trivia = trailing;
188        }
189        Declaration::Ensemble(n) => {
190            n.leading_trivia = leading;
191            n.trailing_trivia = trailing;
192        }
193        Declaration::Session(n) => {
194            n.leading_trivia = leading;
195            n.trailing_trivia = trailing;
196        }
197        Declaration::Topology(n) => {
198            n.leading_trivia = leading;
199            n.trailing_trivia = trailing;
200        }
201        Declaration::Immune(n) => {
202            n.leading_trivia = leading;
203            n.trailing_trivia = trailing;
204        }
205        Declaration::Reflex(n) => {
206            n.leading_trivia = leading;
207            n.trailing_trivia = trailing;
208        }
209        Declaration::Heal(n) => {
210            n.leading_trivia = leading;
211            n.trailing_trivia = trailing;
212        }
213        Declaration::Component(n) => {
214            n.leading_trivia = leading;
215            n.trailing_trivia = trailing;
216        }
217        Declaration::View(n) => {
218            n.leading_trivia = leading;
219            n.trailing_trivia = trailing;
220        }
221        Declaration::Channel(n) => {
222            n.leading_trivia = leading;
223            n.trailing_trivia = trailing;
224        }
225        Declaration::Socket(n) => {
226            n.leading_trivia = leading;
227            n.trailing_trivia = trailing;
228        }
229        Declaration::Upstream(n) => {
230            n.leading_trivia = leading;
231            n.trailing_trivia = trailing;
232        }
233        Declaration::Voice(n) => {
234            n.leading_trivia = leading;
235            n.trailing_trivia = trailing;
236        }
237        Declaration::Cors(n) => {
238            n.leading_trivia = leading;
239            n.trailing_trivia = trailing;
240        }
241        Declaration::Credential(n) => {
242            n.leading_trivia = leading;
243            n.trailing_trivia = trailing;
244        }
245        Declaration::Cache(n) => {
246            n.leading_trivia = leading;
247            n.trailing_trivia = trailing;
248        }
249        Declaration::Savant(n) => {
250            n.leading_trivia = leading;
251            n.trailing_trivia = trailing;
252        }
253        Declaration::Synth(n) => {
254            n.leading_trivia = leading;
255            n.trailing_trivia = trailing;
256        }
257        Declaration::Scope(n) => {
258            n.leading_trivia = leading;
259            n.trailing_trivia = trailing;
260        }
261        Declaration::Observable(n) => {
262            n.leading_trivia = leading;
263            n.trailing_trivia = trailing;
264        }
265        Declaration::Witness(n) => {
266            n.leading_trivia = leading;
267            n.trailing_trivia = trailing;
268        }
269        Declaration::Document(n) => {
270            n.leading_trivia = leading;
271            n.trailing_trivia = trailing;
272        }
273        Declaration::Deliver(n) => {
274            n.leading_trivia = leading;
275            n.trailing_trivia = trailing;
276        }
277        Declaration::Generic(n) => {
278            n.leading_trivia = leading;
279            n.trailing_trivia = trailing;
280        }
281    }
282}
283
284// ── Public error type ────────────────────────────────────────────────────────
285
286/// §Fase 28.d — Source-context constants. D4 ratified 2026-05-10:
287/// 2 lines before + 2 lines after the error line. Mirror of the
288/// Python-side `_SOURCE_CONTEXT_LINES_BEFORE` / `_AFTER` so the
289/// rustc-style block has identical shape across stacks.
290pub const SOURCE_CONTEXT_LINES_BEFORE: usize = 2;
291pub const SOURCE_CONTEXT_LINES_AFTER: usize = 2;
292
293/// §Fase 28.d — Rustc-style source-context block for a parse error.
294///
295/// Holds a reference to the source text plus the line/column the
296/// error points at. Rendering is lazy — call ``render()`` to format
297/// the block (line numbers + caret + 2 lines before + 2 after).
298///
299/// Pure and deterministic: no ANSI colors, no terminal-width
300/// detection. Output shape is byte-identical to the Python
301/// `SourceSnippet.render()` on the same input — that's the cross-
302/// stack drift gate (28.i).
303#[derive(Debug, Clone)]
304pub struct SourceSnippet {
305    pub source: String,
306    pub line: u32,
307    pub column: u32,
308    pub filename: String,
309    pub context_before: usize,
310    pub context_after: usize,
311}
312
313impl SourceSnippet {
314    /// Construct with the default 2/2 context window.
315    pub fn new(source: String, line: u32, column: u32, filename: String) -> Self {
316        Self {
317            source,
318            line,
319            column,
320            filename,
321            context_before: SOURCE_CONTEXT_LINES_BEFORE,
322            context_after: SOURCE_CONTEXT_LINES_AFTER,
323        }
324    }
325
326    /// Format the snippet as a multi-line rustc-style block.
327    ///
328    /// Empty source → empty string. Out-of-range line → empty
329    /// string. Caret column is clamped to `[1, line_len + 1]`.
330    /// Output shape matches Python `SourceSnippet.render` byte-
331    /// identically per D7.
332    #[must_use]
333    pub fn render(&self) -> String {
334        if self.source.is_empty() || self.line < 1 {
335            return String::new();
336        }
337        let raw: Vec<&str> = self.source.split('\n').collect();
338        // Match Python's str.splitlines() trailing-newline shape:
339        // strip an empty trailing entry produced by a final '\n'.
340        let lines: Vec<&str> = if raw.last() == Some(&"") {
341            raw[..raw.len() - 1].to_vec()
342        } else {
343            raw
344        };
345        if lines.is_empty() || self.line as usize > lines.len() {
346            return String::new();
347        }
348
349        let line_idx = self.line as usize;
350        let start = line_idx.saturating_sub(self.context_before).max(1);
351        let end = (line_idx + self.context_after).min(lines.len());
352
353        let gutter = end.to_string().len();
354        let empty_gutter = " ".repeat(gutter);
355
356        let mut out: Vec<String> = Vec::with_capacity(end - start + 4);
357        out.push(format!(
358            "{empty_gutter} --> {}:{}:{}",
359            self.filename, self.line, self.column
360        ));
361        out.push(format!("{empty_gutter} |"));
362        for n in start..=end {
363            let line_text = lines[n - 1];
364            out.push(format!("{n:>gutter$} | {line_text}", gutter = gutter));
365            if n == line_idx {
366                let line_len = line_text.chars().count();
367                let col = (self.column as usize).clamp(1, line_len + 1);
368                out.push(format!(
369                    "{empty_gutter} | {pad}^",
370                    pad = " ".repeat(col - 1)
371                ));
372            }
373        }
374        out.join("\n")
375    }
376}
377
378#[derive(Debug, Clone, Default)]
379pub struct ParseError {
380    pub message: String,
381    pub line: u32,
382    pub column: u32,
383    /// §Fase 28.d — Optional rustc-style source-context block.
384    /// `None` preserves the legacy single-line shape; populated by
385    /// `Parser::with_source` callers (and by `parse_with_recovery`
386    /// / `parse` when a source has been attached to the parser).
387    /// Existing struct-literal call sites use the `..Default::default()`
388    /// idiom (default = None) to stay terse.
389    pub source_snippet: Option<SourceSnippet>,
390}
391
392impl ParseError {
393    /// §Fase 28.d — Attach a `SourceSnippet` derived from raw source
394    /// text and filename. Returns `self` so the call can be chained
395    /// at the construction site. No-op when `line == 0`. Idempotent.
396    #[must_use]
397    pub fn attach_source(mut self, source: &str, filename: &str) -> Self {
398        if self.line >= 1 {
399            self.source_snippet = Some(SourceSnippet::new(
400                source.to_string(),
401                self.line,
402                self.column,
403                filename.to_string(),
404            ));
405        }
406        self
407    }
408}
409
410impl std::fmt::Display for ParseError {
411    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
412        write!(f, "[line {}:{}] {}", self.line, self.column, self.message)?;
413        if let Some(snippet) = &self.source_snippet {
414            let block = snippet.render();
415            if !block.is_empty() {
416                write!(f, "\n{block}")?;
417            }
418        }
419        Ok(())
420    }
421}
422
423impl std::error::Error for ParseError {}
424
425// ── §Fase 28.c — Public recovery result ──────────────────────────────────────
426//
427// Mirror of Python's `axon.compiler.parser.ParseResult` (Fase 28.b).
428// The rationale, sync semantics, and test contract are documented in
429// `docs/fase/fase_28_adopter_diagnostic_robustness.md`. The Rust frontend
430// must produce structurally identical error lists to the Python parser
431// when handed the same source — that is the cross-stack drift gate
432// (D7 ratified 2026-05-10: byte-identical error lists).
433//
434// `program` holds whatever declarations the parser was able to parse
435// successfully. `errors` holds every recovered error in source order.
436// A clean parse returns `errors.is_empty()`; the existing fail-fast
437// `parse()` API is preserved verbatim per D9.
438
439/// Outcome of `Parser::parse_with_recovery` — partial program plus the
440/// list of every error the parser recovered from. See module docs for
441/// the panic-mode + sync-point recovery semantics.
442#[derive(Debug)]
443pub struct ParseResult {
444    pub program: Program,
445    pub errors: Vec<ParseError>,
446}
447
448impl ParseResult {
449    /// True iff at least one parse error was recovered. Callers that
450    /// want to short-circuit on failure should check this rather than
451    /// relying on `program.declarations.is_empty()` (the parser may
452    /// have salvaged some declarations even with errors present).
453    #[inline]
454    #[must_use]
455    pub fn has_errors(&self) -> bool {
456        !self.errors.is_empty()
457    }
458
459    /// Inverse of `has_errors`. Convenience for the "happy path" check
460    /// in tests + adopter integrations.
461    #[inline]
462    #[must_use]
463    pub fn is_clean(&self) -> bool {
464        self.errors.is_empty()
465    }
466}
467
468/// §Fase 28.c — Top-level declaration keywords used as resync points
469/// during error recovery (D2 ratified 2026-05-10). Mirrors the
470/// `_TOP_LEVEL_DECLARATION_KEYWORDS` frozenset on the Python side.
471///
472/// Distinct from `tokens::is_declaration_keyword` because that helper
473/// is used by the structural declaration counter and intentionally
474/// excludes some grammar-only tokens (Know/Believe/Speculate/Doubt,
475/// Ingest, Ots) that DO begin a top-level declaration in
476/// `parse_declaration` and therefore must be valid sync points.
477///
478/// Adding a new top-level dispatch arm in `parse_declaration` MUST
479/// add the corresponding token here so the recovery walker can
480/// re-sync correctly.
481#[inline]
482const fn is_top_level_decl_kw_for_recovery(tt: &TokenType) -> bool {
483    matches!(
484        tt,
485        TokenType::Import
486            | TokenType::Persona
487            | TokenType::Context
488            | TokenType::Anchor
489            | TokenType::Memory
490            | TokenType::Tool
491            | TokenType::Type
492            | TokenType::Flow
493            | TokenType::Intent
494            | TokenType::Run
495            | TokenType::Let
496            | TokenType::Know
497            | TokenType::Believe
498            | TokenType::Speculate
499            | TokenType::Doubt
500            | TokenType::Lambda
501            | TokenType::Agent
502            | TokenType::Shield
503            | TokenType::Pix
504            | TokenType::Ledger
505            | TokenType::Psyche
506            | TokenType::Corpus
507            | TokenType::Dataspace
508            | TokenType::Ots
509            | TokenType::Mandate
510            | TokenType::Compute
511            | TokenType::Daemon
512            // §Fase 87.a/d — the autonomous research primitive + synth policy.
513            | TokenType::Savant
514            | TokenType::Synth
515            // §Fase 88.a — the authorization-scope policy declaration.
516            | TokenType::Scope
517            | TokenType::AxonStore
518            | TokenType::AxonEndpoint
519            | TokenType::Resource
520            | TokenType::Fabric
521            | TokenType::Manifest
522            | TokenType::Observe
523            | TokenType::Reconcile
524            | TokenType::Lease
525            | TokenType::Ensemble
526            | TokenType::Session
527            | TokenType::Topology
528            | TokenType::Immune
529            | TokenType::Reflex
530            | TokenType::Heal
531            | TokenType::Component
532            | TokenType::View
533            | TokenType::Channel
534            | TokenType::Ingest
535            | TokenType::Persist
536            | TokenType::Retrieve
537            | TokenType::Mutate
538            | TokenType::Purge
539            | TokenType::Transact
540            | TokenType::Mcp
541    )
542}
543
544// ── §Fase 30.b — axonendpoint transport + keepalive closed enums ────────────
545//
546// D2 ratified 2026-05-10: `transport` is a closed enum
547// {json, sse, ndjson}. D6 ratified: `keepalive` is a closed enum
548// {5s, 15s, 30s, 60s}. Both mirror the Python frontend's
549// `_AXONENDPOINT_TRANSPORT_VALUES` / `_AXONENDPOINT_KEEPALIVE_VALUES`
550// frozensets in `axon/compiler/parser.py`. Cross-stack drift gate
551// (30.b fixture) asserts byte-identical parse for every entry.
552
553/// Adopter-facing acceptable values for `transport:` field.
554/// Used by both the parser (validation + smart-suggest) and the
555/// type-checker (30.c) so adopter tooling sees one canonical list.
556pub const AXONENDPOINT_TRANSPORT_VALUES: &[&str] = &["json", "sse", "ndjson"];
557
558/// §Fase 33.z.k.b (v1.28.0) — Closed-catalog SSE wire-format
559/// dialects. Selected via the parametrized grammar
560/// `transport: sse(<dialect>)`; bare `transport: sse` resolves to
561/// the Q1 default per the flow's algebraic-effect predicate
562/// (openai for tool-streaming flows; axon for type-annotation-only).
563///
564/// Vertical-grounded scope (Q3 revised 2026-05-14): five dialects
565/// cover ~99% of LLM-streaming adopter expectations.
566///   - `axon`      — current W3C named events
567///                   (event: axon.token / event: axon.complete).
568///                   D6 backwards-compat baseline; indefinitely
569///                   supported as a first-class option.
570///   - `openai`    — `data: {"choices":[{"delta":{...}}]}` frames
571///                   terminated by `data: [DONE]`. OpenAI Chat
572///                   Completions streaming wire verbatim.
573///   - `kimi`      — Moonshot Kimi (kimi.moonshot.cn) — uses the
574///                   OpenAI-compatible Chat Completions wire format
575///                   verbatim (same chunk shape, same `data: [DONE]`
576///                   sentinel). First-class entry so adopters
577///                   declare intent explicitly; under the hood the
578///                   wire is identical to `openai`.
579///   - `glm`       — Zhipu ChatGLM (open.bigmodel.cn) — same as
580///                   kimi, uses OpenAI-compat wire. First-class
581///                   entry for adopter clarity.
582///   - `anthropic` — `event: content_block_delta` frames terminated
583///                   by `event: message_stop`. Adopter SDKs
584///                   targeting Anthropic Claude consume this shape
585///                   verbatim.
586///
587/// Why kimi + glm as first-class entries (Q3 revision rationale):
588/// Bemarking AI's primary adopter pipelines through Kimi K2.x +
589/// Zhipu GLM-4.x. While the wire IS byte-identical to OpenAI's
590/// Chat Completions streaming, declaring `transport: sse(kimi)` /
591/// `transport: sse(glm)` lets the audit trail + observability
592/// surfaces correlate adopter intent against the underlying
593/// provider — without the adopter having to know that "kimi
594/// happens to be OpenAI-compat on the wire today". The runtime
595/// dispatches kimi + glm to the same `OpenAIDialectAdapter` so
596/// the wire shape stays canonical-OpenAI-bytes.
597///
598/// Open-set adapter pluggability (downstream crates registering
599/// custom dialects) remains explicitly out of scope per the
600/// Axon-for-Axon discipline.
601pub const AXONENDPOINT_TRANSPORT_DIALECTS: &[&str] =
602    &["axon", "openai", "kimi", "glm", "anthropic"];
603
604/// Adopter-facing acceptable values for `keepalive:` field.
605pub const AXONENDPOINT_KEEPALIVE_VALUES: &[&str] = &["5s", "15s", "30s", "60s"];
606
607/// §Fase 32.b D3 — Closed method enum for `method:` field. Adopter-
608/// declarable methods only; HEAD/OPTIONS/CONNECT/TRACE are
609/// runtime-managed (CORS preflight, etc.) and never declared from
610/// source. Closed enum refuses interpretation drift; smart-suggest
611/// catches near-misses at parse time.
612///
613/// §Fase 107.a — `QUERY` (RFC 10008, Proposed Standard, June 2026): the safe +
614/// idempotent + cacheable method that CARRIES A REQUEST BODY — the first new HTTP
615/// method in two decades. It carries a LAW, not just a route: `axon-T927` refuses
616/// at compile time a QUERY endpoint whose flow performs a declared write (the
617/// RFC's normative "safe and idempotent" MUST, made a proof).
618///
619/// Must stay in lockstep with `type_checker::VALID_ENDPOINT_METHODS`.
620pub const AXONENDPOINT_METHOD_VALUES: &[&str] =
621    &["GET", "POST", "PUT", "DELETE", "PATCH", "QUERY"];
622
623/// §Fase 36.d (D2) — Closed catalog for the `axonendpoint backend:`
624/// declaration. The set is `CANONICAL_PROVIDERS ∪ {auto, stub}`:
625///
626///   - the seven canonical LLM providers — `anthropic`, `gemini`,
627///     `glm`, `kimi`, `ollama`, `openai`, `openrouter` — a concrete,
628///     declared backend that rung 2 of the Fase 36 D1 resolution
629///     ladder fires immediately;
630///   - `auto` — transparent: declaring it is equivalent to omitting
631///     `backend:` entirely (the route resolves down the ladder —
632///     server default → environment-available providers);
633///   - `stub` — the no-op backend, reachable ONLY by an explicit,
634///     written declaration (D5: a silent degradation to `stub` is
635///     forbidden; an explicit opt-in is not).
636///
637/// `axon-frontend` carries zero runtime deps and therefore cannot
638/// import `axon::backends::CANONICAL_PROVIDERS`; this list is a
639/// hand-maintained mirror. The axon-rs drift gate
640/// (`tests/fase36_d_backend_catalog_drift.rs`) asserts the two stay
641/// byte-identical — adding a provider in one place without the other
642/// fails CI.
643pub const AXONENDPOINT_BACKEND_VALUES: &[&str] = &[
644    "anthropic",
645    "auto",
646    "gemini",
647    "glm",
648    "kimi",
649    "ollama",
650    "openai",
651    "openrouter",
652    "stub",
653];
654
655#[inline]
656fn axonendpoint_is_valid_transport(s: &str) -> bool {
657    AXONENDPOINT_TRANSPORT_VALUES.iter().any(|&v| v == s)
658}
659
660#[inline]
661fn axonendpoint_is_valid_method(s: &str) -> bool {
662    AXONENDPOINT_METHOD_VALUES.iter().any(|&v| v == s)
663}
664
665#[inline]
666fn axonendpoint_is_valid_backend(s: &str) -> bool {
667    AXONENDPOINT_BACKEND_VALUES.iter().any(|&v| v == s)
668}
669
670#[inline]
671fn axonendpoint_is_valid_keepalive(s: &str) -> bool {
672    AXONENDPOINT_KEEPALIVE_VALUES.iter().any(|&v| v == s)
673}
674
675/// §Fase 37.y (D2) — Closed type catalog for query parameters.
676///
677/// Query values arrive over HTTP as URL-encoded strings; the catalog
678/// is the set of types axon will validate / coerce them into for the
679/// Request Binding Contract. Hand-curated, intentionally small:
680///   - `Text` — the raw string (always succeeds)
681///   - `Int` — `i64` parseable
682///   - `Float` — `f64` parseable, finite
683///   - `Bool` — case-insensitive `{true, false, 1, 0, yes, no, on, off}`
684///   - `Uuid` — RFC 4122 textual form
685///
686/// Extending the catalog is a future axon-T?nn surface; v1.38.5 ships
687/// the 5 types covering ~95% of REST query patterns. Lists / dates /
688/// datetimes / enums are honest deferrals (see §7 of the plan vivo).
689pub const AXONENDPOINT_QUERY_PARAM_TYPES: &[&str] =
690    &["Text", "Int", "Float", "Bool", "Uuid"];
691
692/// `true` iff `s` is one of the §Fase 37.y (D2) query-param catalog
693/// entries — exact case-sensitive match (axon types are PascalCase).
694#[inline]
695pub(crate) fn axonendpoint_is_valid_query_param_type(s: &str) -> bool {
696    AXONENDPOINT_QUERY_PARAM_TYPES.iter().any(|&v| v == s)
697}
698
699/// §Fase 37.y (D1) — Extract `{name}` placeholder names from an
700/// `axonendpoint` `path:` string, in left-to-right declaration order.
701///
702/// Recognized placeholder grammar (single-segment, no nested braces):
703/// `{NAME}` where `NAME` matches `[A-Za-z_][A-Za-z0-9_]*`. Anything
704/// inside braces that does NOT match the identifier shape is silently
705/// IGNORED — it's either an adopter typo (caught later by axum at
706/// route registration) or a literal brace in the URL pattern.
707///
708/// Returns `Err(duplicate_name)` when the same `{name}` appears more
709/// than once in the path — HTTP route patterns reject duplicates
710/// structurally (`axum` would panic at registration), so surfacing
711/// the error at parse time is the right place.
712///
713/// Pure + total: never panics; deterministic over its single string
714/// argument. Hand-rolled scanner (no regex dep at parser layer).
715///
716/// # Examples
717///
718/// - `"/api/users"` → `Ok(vec![])`
719/// - `"/api/users/{id}"` → `Ok(vec!["id"])`
720/// - `"/api/tenants/{tenant_id}/secrets/{secret_name}"`
721///   → `Ok(vec!["tenant_id", "secret_name"])`
722/// - `"/api/users/{id}/posts/{id}"` → `Err("id")` (duplicate)
723/// - `"/api/{not valid}"` → `Ok(vec![])` (malformed brace content
724///   silently ignored; axum surfaces the error at registration)
725pub(crate) fn extract_path_param_names(path: &str) -> Result<Vec<String>, String> {
726    let mut out: Vec<String> = Vec::new();
727    let bytes = path.as_bytes();
728    let mut i = 0;
729    while i < bytes.len() {
730        if bytes[i] != b'{' {
731            i += 1;
732            continue;
733        }
734        // Find the matching close brace; if none, the open brace is
735        // a literal — leave it alone.
736        let start = i + 1;
737        let mut end = start;
738        while end < bytes.len() && bytes[end] != b'}' {
739            end += 1;
740        }
741        if end == bytes.len() {
742            // Unterminated — give up; downstream parser/runtime
743            // surface the malformed path elsewhere.
744            break;
745        }
746        let raw = &path[start..end];
747        // Validate identifier shape: [A-Za-z_][A-Za-z0-9_]*
748        let valid = !raw.is_empty()
749            && raw.bytes().enumerate().all(|(idx, b)| {
750                if idx == 0 {
751                    b.is_ascii_alphabetic() || b == b'_'
752                } else {
753                    b.is_ascii_alphanumeric() || b == b'_'
754                }
755            });
756        if valid {
757            let name = raw.to_string();
758            if out.iter().any(|existing| existing == &name) {
759                return Err(name);
760            }
761            out.push(name);
762        }
763        i = end + 1;
764    }
765    Ok(out)
766}
767
768/// §Fase 32.g (D8) — Closed capability-slug grammar. Validates a
769/// `requires:` slug per `^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$`.
770///
771/// Hand-rolled (no regex dep at parser layer) — each segment must
772/// match `[a-z][a-z0-9_]*` and segments are joined by single dots.
773/// Public so the runtime mirror (`axon::auth_scope`) reuses the same
774/// predicate without duplicating the rule.
775///
776/// Examples valid: `admin`, `legal.read`, `hipaa.phi.read`,
777/// `bank.officer.senior`, `a`, `a_b`, `a1`.
778/// Examples invalid: empty, `Admin` (uppercase), `1admin` (digit
779/// first), `bank-officer` (hyphen), `bank..a` (empty segment),
780/// `.admin`, `admin.`, `admin..` .
781pub fn is_valid_capability_slug(slug: &str) -> bool {
782    if slug.is_empty() {
783        return false;
784    }
785    for segment in slug.split('.') {
786        if !is_valid_slug_segment(segment) {
787            return false;
788        }
789    }
790    true
791}
792
793fn is_valid_slug_segment(seg: &str) -> bool {
794    let mut chars = seg.chars();
795    let first = match chars.next() {
796        Some(c) => c,
797        None => return false,
798    };
799    if !first.is_ascii_lowercase() {
800        return false;
801    }
802    chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
803}
804
805// ════════════════════════════════════════════════════════════════════
806//  §Fase 37.y (D1) — `extract_path_param_names` unit tests
807// ════════════════════════════════════════════════════════════════════
808
809// ════════════════════════════════════════════════════════════════════
810//  §Fase 37.y (D2) — `axonendpoint_is_valid_query_param_type` + the
811//  inline `query: { … }` parser, end-to-end through the lexer.
812// ════════════════════════════════════════════════════════════════════
813
814#[cfg(test)]
815mod query_param_catalog_tests {
816    use super::{axonendpoint_is_valid_query_param_type, AXONENDPOINT_QUERY_PARAM_TYPES};
817
818    #[test]
819    fn accepts_every_catalog_entry() {
820        for ty in AXONENDPOINT_QUERY_PARAM_TYPES {
821            assert!(
822                axonendpoint_is_valid_query_param_type(ty),
823                "catalog entry `{ty}` must validate"
824            );
825        }
826    }
827
828    #[test]
829    fn rejects_off_catalog_types() {
830        for off in &[
831            "Timestamp",    // not in v1.38.5 — list/dates deferred
832            "Date",
833            "DateTime",
834            "List<Text>",   // multi-value query params deferred (§7)
835            "Jsonb",        // store-only types not query-applicable
836            "Bytea",
837            "text",         // lowercase rejected (axon types are PascalCase)
838            "TEXT",
839            "Number",       // not in axon's type catalog at all
840            "",             // empty
841            " ",            // whitespace
842        ] {
843            assert!(
844                !axonendpoint_is_valid_query_param_type(off),
845                "off-catalog `{off}` must reject"
846            );
847        }
848    }
849
850    #[test]
851    fn catalog_size_matches_design() {
852        // The plan vivo D2 states a closed 5-type catalog. A future
853        // axon-T?nn surface may extend it; that requires updating BOTH
854        // the catalog AND the plan vivo §7 honest-scope note.
855        assert_eq!(AXONENDPOINT_QUERY_PARAM_TYPES.len(), 5);
856    }
857}
858
859#[cfg(test)]
860mod query_param_parser_tests {
861    use crate::lexer::Lexer;
862    use crate::parser::Parser;
863
864    fn parse_endpoint_source(src: &str) -> Result<crate::ast::AxonEndpointDefinition, String> {
865        let tokens = Lexer::new(src, "test.axon")
866            .tokenize()
867            .map_err(|e| format!("lex: {}", e.message))?;
868        let mut parser = Parser::new(tokens);
869        let program = parser.parse().map_err(|e| format!("parse: {}", e.message))?;
870        program
871            .declarations
872            .into_iter()
873            .find_map(|d| match d {
874                crate::ast::Declaration::AxonEndpoint(e) => Some(e),
875                _ => None,
876            })
877            .ok_or_else(|| "no axonendpoint in program".to_string())
878    }
879
880    #[test]
881    fn endpoint_with_no_query_block_keeps_empty_vec() {
882        let src = r#"
883            axonendpoint write_secret {
884                method: POST
885                path: "/api/users"
886                body: SecretWriteRequest
887                execute: WriteSecret
888            }
889        "#;
890        let ep = parse_endpoint_source(src).expect("parses");
891        assert!(
892            ep.query_params.is_empty(),
893            "D5 — no `query:` block ⇒ empty query_params"
894        );
895    }
896
897    #[test]
898    fn single_query_param_required() {
899        let src = r#"
900            axonendpoint list_users {
901                method: GET
902                path: "/api/users"
903                query: { status: Text }
904                execute: ListUsers
905            }
906        "#;
907        let ep = parse_endpoint_source(src).expect("parses");
908        assert_eq!(ep.query_params.len(), 1);
909        assert_eq!(ep.query_params[0].name, "status");
910        assert_eq!(ep.query_params[0].type_expr.name, "Text");
911        assert!(!ep.query_params[0].type_expr.optional);
912    }
913
914    #[test]
915    fn optional_query_param_via_question_suffix() {
916        let src = r#"
917            axonendpoint list_users {
918                method: GET
919                path: "/api/users"
920                query: { limit: Int? }
921                execute: ListUsers
922            }
923        "#;
924        let ep = parse_endpoint_source(src).expect("parses");
925        assert_eq!(ep.query_params.len(), 1);
926        assert_eq!(ep.query_params[0].name, "limit");
927        assert_eq!(ep.query_params[0].type_expr.name, "Int");
928        assert!(
929            ep.query_params[0].type_expr.optional,
930            "`?` suffix sets optional"
931        );
932    }
933
934    #[test]
935    fn multiple_query_params_preserve_declaration_order() {
936        let src = r#"
937            axonendpoint search {
938                method: GET
939                path: "/api/search"
940                query: { q: Text, page: Int?, limit: Int?, exact: Bool? }
941                execute: Search
942            }
943        "#;
944        let ep = parse_endpoint_source(src).expect("parses");
945        let names: Vec<&str> = ep.query_params.iter().map(|f| f.name.as_str()).collect();
946        assert_eq!(names, vec!["q", "page", "limit", "exact"]);
947        let types: Vec<&str> = ep
948            .query_params
949            .iter()
950            .map(|f| f.type_expr.name.as_str())
951            .collect();
952        assert_eq!(types, vec!["Text", "Int", "Int", "Bool"]);
953        let optionals: Vec<bool> = ep
954            .query_params
955            .iter()
956            .map(|f| f.type_expr.optional)
957            .collect();
958        assert_eq!(optionals, vec![false, true, true, true]);
959    }
960
961    #[test]
962    fn duplicate_query_param_is_parse_error() {
963        let src = r#"
964            axonendpoint bad {
965                method: GET
966                path: "/api/x"
967                query: { name: Text, name: Int? }
968                execute: Bad
969            }
970        "#;
971        let err = parse_endpoint_source(src).expect_err("must fail");
972        assert!(
973            err.contains("duplicate query param 'name'"),
974            "error must name the duplicate. Got: {err}"
975        );
976    }
977
978    #[test]
979    fn off_catalog_type_with_smart_suggest_hint() {
980        // `Strng` is one edit away from `Text` (would suggest `Text`?
981        // Actually edit distance to `Text` is 4; to `Int` is 5. Likely
982        // no smart suggestion within distance 2. The error still names
983        // the catalog explicitly.)
984        let src = r#"
985            axonendpoint bad {
986                method: GET
987                path: "/api/x"
988                query: { value: Strng }
989                execute: Bad
990            }
991        "#;
992        let err = parse_endpoint_source(src).expect_err("must fail");
993        assert!(
994            err.contains("unsupported type 'Strng'"),
995            "error must name the bad type. Got: {err}"
996        );
997        assert!(
998            err.contains("Expected one of: Text | Int | Float | Bool | Uuid"),
999            "error must list the closed catalog. Got: {err}"
1000        );
1001    }
1002
1003    #[test]
1004    fn close_typo_gets_did_you_mean_hint() {
1005        // `Txt` → edit distance 1 from `Text` → smart-suggest should
1006        // surface the hint.
1007        let src = r#"
1008            axonendpoint bad {
1009                method: GET
1010                path: "/api/x"
1011                query: { value: Txt }
1012                execute: Bad
1013            }
1014        "#;
1015        let err = parse_endpoint_source(src).expect_err("must fail");
1016        assert!(
1017            err.contains("Did you mean") && err.contains("`Text`"),
1018            "smart-suggest must hint `Text`. Got: {err}"
1019        );
1020    }
1021
1022    #[test]
1023    fn every_catalog_type_parses_cleanly() {
1024        // Round-trip smoke for all 5 catalog entries.
1025        for ty in &["Text", "Int", "Float", "Bool", "Uuid"] {
1026            let src = format!(
1027                r#"
1028                    axonendpoint x {{
1029                        method: GET
1030                        path: "/api/x"
1031                        query: {{ v: {ty} }}
1032                        execute: X
1033                    }}
1034                "#
1035            );
1036            let ep = parse_endpoint_source(&src)
1037                .unwrap_or_else(|e| panic!("`{ty}` should parse: {e}"));
1038            assert_eq!(ep.query_params[0].type_expr.name, *ty);
1039        }
1040    }
1041
1042    #[test]
1043    fn comma_optional_between_params() {
1044        // The plan vivo design accepts both comma-separated and
1045        // whitespace-separated query params (existing parser style is
1046        // forgiving). Whitespace-only:
1047        let src = r#"
1048            axonendpoint x {
1049                method: GET
1050                path: "/api/x"
1051                query: { a: Text b: Int? }
1052                execute: X
1053            }
1054        "#;
1055        let ep = parse_endpoint_source(src).expect("parses without commas");
1056        assert_eq!(ep.query_params.len(), 2);
1057    }
1058
1059    // ─── Robustness hardening (37.y.2 100% robust closure) ──────────
1060
1061    #[test]
1062    fn double_query_block_is_parse_error() {
1063        // An adopter who copy-pastes the `query:` block twice should
1064        // see a clear parse error, not a silent merge that produces
1065        // an unexpectedly-augmented endpoint with both blocks fused.
1066        let src = r#"
1067            axonendpoint x {
1068                method: GET
1069                path: "/api/x"
1070                query: { a: Text }
1071                query: { b: Int? }
1072                execute: X
1073            }
1074        "#;
1075        let err = parse_endpoint_source(src).expect_err("must fail");
1076        assert!(
1077            err.contains("declares `query: { … }` more than once"),
1078            "error must call out the duplicate block. Got: {err}"
1079        );
1080        assert!(
1081            err.contains("combine all params into a single block"),
1082            "error must hint the canonical fix. Got: {err}"
1083        );
1084    }
1085
1086    #[test]
1087    fn optional_generic_type_is_parse_error_with_canonical_hint() {
1088        // `Optional<Text>` is the wrong way to declare an optional
1089        // query param. The canonical syntax is `Text?` (the `?`
1090        // suffix). The error must surface this with a literal example.
1091        let src = r#"
1092            axonendpoint x {
1093                method: GET
1094                path: "/api/x"
1095                query: { value: Optional<Text> }
1096                execute: X
1097            }
1098        "#;
1099        let err = parse_endpoint_source(src).expect_err("must fail");
1100        assert!(
1101            err.contains("generic type `Optional<Text>`"),
1102            "error must name the generic type literally. Got: {err}"
1103        );
1104        assert!(
1105            err.contains("Use `Text?` (the `?` suffix)"),
1106            "error must hint the canonical `Text?` syntax. Got: {err}"
1107        );
1108    }
1109
1110    #[test]
1111    fn list_generic_type_is_parse_error_with_deferral_hint() {
1112        // Multi-value query params (`?tag=a&tag=b`) are honest-
1113        // deferred per the plan vivo §7. Adopters who write
1114        // `List<Text>` should see a clear error explaining the
1115        // deferral, not a confusing "type `List` not in catalog".
1116        let src = r#"
1117            axonendpoint x {
1118                method: GET
1119                path: "/api/x"
1120                query: { tags: List<Text> }
1121                execute: X
1122            }
1123        "#;
1124        let err = parse_endpoint_source(src).expect_err("must fail");
1125        assert!(
1126            err.contains("generic type `List<Text>`"),
1127            "error must name the generic type. Got: {err}"
1128        );
1129        assert!(
1130            err.contains("Multi-value query params")
1131                && err.contains("honest-deferred"),
1132            "error must mention the multi-value deferral. Got: {err}"
1133        );
1134    }
1135
1136    #[test]
1137    fn other_generic_types_caught_generically() {
1138        // Generic types beyond `Optional` and `List` get the
1139        // generic-rejection message without a canonical-syntax hint
1140        // (the catalog list is the canonical guidance).
1141        let src = r#"
1142            axonendpoint x {
1143                method: GET
1144                path: "/api/x"
1145                query: { value: Stream<Int> }
1146                execute: X
1147            }
1148        "#;
1149        let err = parse_endpoint_source(src).expect_err("must fail");
1150        assert!(
1151            err.contains("generic type `Stream<Int>`"),
1152            "error must name the generic type. Got: {err}"
1153        );
1154        assert!(
1155            err.contains("Text | Int | Float | Bool | Uuid"),
1156            "error must list the closed catalog. Got: {err}"
1157        );
1158    }
1159
1160    #[test]
1161    fn uuid_optional_parses_cleanly() {
1162        // Hardening companion — `Uuid?` is in the catalog AND
1163        // optional. The two features compose without surprise.
1164        let src = r#"
1165            axonendpoint find {
1166                method: GET
1167                path: "/api/x"
1168                query: { after: Uuid? }
1169                execute: Find
1170            }
1171        "#;
1172        let ep = parse_endpoint_source(src).expect("parses");
1173        assert_eq!(ep.query_params.len(), 1);
1174        assert_eq!(ep.query_params[0].name, "after");
1175        assert_eq!(ep.query_params[0].type_expr.name, "Uuid");
1176        assert!(ep.query_params[0].type_expr.optional);
1177        assert_eq!(ep.query_params[0].type_expr.generic_param, "");
1178    }
1179
1180    #[test]
1181    fn empty_query_block_yields_empty_vec() {
1182        // `query: { }` is grammatically valid but semantically a
1183        // no-op (equivalent to omitting the block). Don't error;
1184        // just record an empty Vec.
1185        let src = r#"
1186            axonendpoint x {
1187                method: GET
1188                path: "/api/x"
1189                query: { }
1190                execute: X
1191            }
1192        "#;
1193        let ep = parse_endpoint_source(src).expect("empty block parses");
1194        assert!(ep.query_params.is_empty());
1195    }
1196
1197    #[test]
1198    fn kivi_secret_write_path_plus_query() {
1199        // Combined path-param + query-param test: an endpoint that
1200        // takes IDs in the URL AND optional filters in the query
1201        // string. This is the natural REST shape Fase 37.y serves.
1202        let src = r#"
1203            axonendpoint write_secret {
1204                method: POST
1205                path: "/api/tenants/{tenant_id}/secrets/{secret_name}"
1206                query: { dry_run: Bool?, overwrite: Bool? }
1207                body: SecretWriteRequest
1208                execute: WriteSecret
1209            }
1210        "#;
1211        let ep = parse_endpoint_source(src).expect("parses");
1212        // Path params populated (from 37.y.1):
1213        assert_eq!(ep.path_params, vec!["tenant_id", "secret_name"]);
1214        // Query params populated (from this sub-fase 37.y.2):
1215        assert_eq!(ep.query_params.len(), 2);
1216        assert_eq!(ep.query_params[0].name, "dry_run");
1217        assert_eq!(ep.query_params[0].type_expr.name, "Bool");
1218        assert!(ep.query_params[0].type_expr.optional);
1219        assert_eq!(ep.query_params[1].name, "overwrite");
1220        // Body still works:
1221        assert_eq!(ep.body_type, "SecretWriteRequest");
1222    }
1223}
1224
1225#[cfg(test)]
1226mod path_param_extraction_tests {
1227    use super::extract_path_param_names;
1228
1229    #[test]
1230    fn empty_path_no_placeholders() {
1231        assert_eq!(extract_path_param_names("/api/users"), Ok(vec![]));
1232        assert_eq!(extract_path_param_names("/"), Ok(vec![]));
1233        assert_eq!(extract_path_param_names(""), Ok(vec![]));
1234    }
1235
1236    #[test]
1237    fn single_placeholder() {
1238        assert_eq!(
1239            extract_path_param_names("/api/users/{id}"),
1240            Ok(vec!["id".to_string()])
1241        );
1242    }
1243
1244    #[test]
1245    fn multiple_placeholders_in_declaration_order() {
1246        assert_eq!(
1247            extract_path_param_names(
1248                "/api/tenants/{tenant_id}/secrets/{secret_name}"
1249            ),
1250            Ok(vec![
1251                "tenant_id".to_string(),
1252                "secret_name".to_string(),
1253            ])
1254        );
1255    }
1256
1257    #[test]
1258    fn kivi_chat_history_path_pattern() {
1259        // The exact pattern the kivi adopter reported (2026-05-20):
1260        // POST /api/tenants/{tenant_id}/secrets/{secret_name}
1261        // Both names extracted in source order.
1262        let names = extract_path_param_names(
1263            "/api/tenants/{tenant_id}/secrets/{secret_name}",
1264        );
1265        assert_eq!(
1266            names,
1267            Ok(vec![
1268                "tenant_id".to_string(),
1269                "secret_name".to_string(),
1270            ])
1271        );
1272    }
1273
1274    #[test]
1275    fn duplicate_placeholder_returns_err() {
1276        assert_eq!(
1277            extract_path_param_names("/api/users/{id}/posts/{id}"),
1278            Err("id".to_string())
1279        );
1280    }
1281
1282    #[test]
1283    fn underscore_and_numeric_in_name() {
1284        assert_eq!(
1285            extract_path_param_names("/api/{user_id}/items/{item_2}"),
1286            Ok(vec!["user_id".to_string(), "item_2".to_string()])
1287        );
1288    }
1289
1290    #[test]
1291    fn leading_underscore_accepted() {
1292        // Identifiers in HTTP paths often start with letters but the
1293        // grammar permits leading underscore (parity with Rust identifier
1294        // rules). The flow parameter name on the binding side has to
1295        // match exactly, so adopters with `_internal_id` in the path
1296        // can pair it with a same-named flow param.
1297        assert_eq!(
1298            extract_path_param_names("/api/{_internal}"),
1299            Ok(vec!["_internal".to_string()])
1300        );
1301    }
1302
1303    #[test]
1304    fn malformed_placeholder_silently_ignored() {
1305        // Content inside `{...}` that does not match the identifier
1306        // grammar is skipped at this layer. axum surfaces the route
1307        // registration failure if the literal text is invalid.
1308        assert_eq!(
1309            extract_path_param_names("/api/{not valid}"),
1310            Ok(vec![])
1311        );
1312        // Empty braces — same: skip silently.
1313        assert_eq!(extract_path_param_names("/api/{}"), Ok(vec![]));
1314        // Mixed: malformed brace skipped, valid placeholder kept.
1315        assert_eq!(
1316            extract_path_param_names("/api/{tenant id}/users/{id}"),
1317            Ok(vec!["id".to_string()])
1318        );
1319    }
1320
1321    #[test]
1322    fn unterminated_brace_returns_clean() {
1323        // Open brace with no close brace — give up without panicking.
1324        // (axum surfaces the malformed-route error at registration.)
1325        assert_eq!(extract_path_param_names("/api/{id"), Ok(vec![]));
1326    }
1327
1328    #[test]
1329    fn placeholders_at_path_boundaries() {
1330        // Placeholder as the very first segment AND the very last
1331        // segment — both should be extracted.
1332        assert_eq!(
1333            extract_path_param_names("{prefix}/api/users/{id}"),
1334            Ok(vec!["prefix".to_string(), "id".to_string()])
1335        );
1336        assert_eq!(
1337            extract_path_param_names("/api/{id}"),
1338            Ok(vec!["id".to_string()])
1339        );
1340    }
1341
1342    #[test]
1343    fn deduplication_detects_non_adjacent_duplicates() {
1344        // The duplicate-detection sweep is global, not just adjacent.
1345        assert_eq!(
1346            extract_path_param_names(
1347                "/api/orgs/{org_id}/teams/{team_id}/repos/{org_id}"
1348            ),
1349            Err("org_id".to_string())
1350        );
1351    }
1352
1353    #[test]
1354    fn never_panics_on_arbitrary_input() {
1355        // Light fuzz: a handful of weird inputs return cleanly.
1356        for input in &[
1357            "{",
1358            "}",
1359            "{}",
1360            "{{}}",
1361            "{{{",
1362            "/api/{}/{id}",
1363            "////",
1364            "\u{1F4A1}",        // emoji (lightbulb)
1365            "\u{0000}",         // null byte
1366        ] {
1367            let _ = extract_path_param_names(input); // must not panic
1368        }
1369    }
1370}
1371
1372#[cfg(test)]
1373mod capability_slug_tests {
1374    use super::is_valid_capability_slug;
1375
1376    #[test]
1377    fn accepts_canonical_examples() {
1378        assert!(is_valid_capability_slug("admin"));
1379        assert!(is_valid_capability_slug("legal.read"));
1380        assert!(is_valid_capability_slug("hipaa.phi.read"));
1381        assert!(is_valid_capability_slug("bank.officer.senior"));
1382        assert!(is_valid_capability_slug("a"));
1383        assert!(is_valid_capability_slug("a_b"));
1384        assert!(is_valid_capability_slug("a1"));
1385        assert!(is_valid_capability_slug("a.b1_c"));
1386    }
1387
1388    #[test]
1389    fn rejects_empty_string() {
1390        assert!(!is_valid_capability_slug(""));
1391    }
1392
1393    #[test]
1394    fn rejects_uppercase() {
1395        assert!(!is_valid_capability_slug("Admin"));
1396        assert!(!is_valid_capability_slug("admin.READ"));
1397    }
1398
1399    #[test]
1400    fn rejects_digit_first() {
1401        assert!(!is_valid_capability_slug("1admin"));
1402        assert!(!is_valid_capability_slug("admin.1read"));
1403    }
1404
1405    #[test]
1406    fn rejects_hyphen() {
1407        assert!(!is_valid_capability_slug("bank-officer"));
1408    }
1409
1410    #[test]
1411    fn rejects_empty_segments() {
1412        assert!(!is_valid_capability_slug("bank..a"));
1413        assert!(!is_valid_capability_slug(".admin"));
1414        assert!(!is_valid_capability_slug("admin."));
1415    }
1416
1417    #[test]
1418    fn rejects_special_chars() {
1419        assert!(!is_valid_capability_slug("admin@read"));
1420        assert!(!is_valid_capability_slug("admin/read"));
1421        assert!(!is_valid_capability_slug("admin read"));
1422    }
1423}
1424
1425// ── Parser ───────────────────────────────────────────────────────────────────
1426
1427pub struct Parser {
1428    tokens: Vec<Token>,
1429    pos: usize,
1430    /// Fase 14.a — leading trivia parallel array, indexed by the
1431    /// effective-token position. `leading_trivia[i]` is the comment
1432    /// trivia that appeared between the previous effective token (or
1433    /// file start) and `tokens[i]`.
1434    leading_trivia: Vec<Vec<Trivia>>,
1435    /// Fase 14.a — trailing trivia parallel array. `trailing_trivia[i]`
1436    /// is the comment trivia on the same line as `tokens[i]`, before
1437    /// the next effective token. Populated by the constructor.
1438    trailing_trivia: Vec<Vec<Trivia>>,
1439    /// Fase 17.a — side-channel for tagging let value_kind. Set by
1440    /// `parse_let_atom` / `parse_let_value_expr` as they descend; read
1441    /// at the end of `parse_let` and stored on the LetStatement.
1442    last_let_value_kind: String,
1443    /// Fase 19.e — loop nesting depth for break/continue scope check.
1444    /// Incremented at the start of `parse_for_in`, decremented after.
1445    /// `parse_break`/`parse_continue` raise ParseError when this is
1446    /// zero (the keyword has no meaning outside a loop body).
1447    loop_depth: u32,
1448    /// §Fase 28.d — Optional source text + filename for the rustc-
1449    /// style source-context block on `ParseError`. Set via the
1450    /// fluent `Parser::with_source` builder; default `None` keeps
1451    /// existing callers (`Parser::new(tokens).parse()`) emitting
1452    /// the legacy single-line shape.
1453    source: Option<String>,
1454    filename: String,
1455}
1456
1457impl Parser {
1458    pub fn new(raw_tokens: Vec<Token>) -> Self {
1459        // ── Fase 14.a — split the raw token stream into:
1460        //   - effective tokens the grammar consumes (cursor advances
1461        //     over these as before),
1462        //   - parallel `leading_trivia` / `trailing_trivia` arrays
1463        //     indexed by effective-token position.
1464        // Comments on a fresh line attach as leading trivia of the
1465        // next effective token; comments on the same line as an
1466        // effective token attach as trailing trivia of that token.
1467        // Roslyn/Swift convention.
1468        let mut effective: Vec<Token> = Vec::with_capacity(raw_tokens.len());
1469        let mut leading: Vec<Vec<Trivia>> = Vec::with_capacity(raw_tokens.len());
1470        let mut trailing: Vec<Vec<Trivia>> = Vec::with_capacity(raw_tokens.len());
1471
1472        let mut pending_leading: Vec<Trivia> = Vec::new();
1473        let mut last_effective_line: i64 = -1;
1474        for tok in raw_tokens {
1475            if is_comment_token(&tok.ttype) {
1476                let kind = token_to_trivia_kind(&tok.ttype)
1477                    .expect("comment token must map to a trivia kind");
1478                let triv = Trivia {
1479                    kind,
1480                    text: tok.value,
1481                    line: tok.line,
1482                    column: tok.column,
1483                };
1484                if !effective.is_empty() && (tok.line as i64) == last_effective_line {
1485                    trailing.last_mut().unwrap().push(triv);
1486                } else {
1487                    pending_leading.push(triv);
1488                }
1489            } else {
1490                last_effective_line = tok.line as i64;
1491                effective.push(tok);
1492                leading.push(std::mem::take(&mut pending_leading));
1493                trailing.push(Vec::new());
1494            }
1495        }
1496
1497        Parser {
1498            tokens: effective,
1499            pos: 0,
1500            leading_trivia: leading,
1501            trailing_trivia: trailing,
1502            last_let_value_kind: "literal".to_string(),
1503            loop_depth: 0,
1504            source: None,
1505            filename: "<source>".to_string(),
1506        }
1507    }
1508
1509    /// §Fase 28.d — Fluent attach of source text + filename for
1510    /// rustc-style source-context blocks on emitted `ParseError`s.
1511    /// Returns `self` so it chains with `.parse_with_recovery()`:
1512    ///
1513    /// ```ignore
1514    /// let result = Parser::new(tokens)
1515    ///     .with_source(src, "foo.axon")
1516    ///     .parse_with_recovery();
1517    /// ```
1518    ///
1519    /// No-op of any other behaviour — pure metadata attach.
1520    #[must_use]
1521    pub fn with_source(mut self, source: &str, filename: &str) -> Self {
1522        self.source = Some(source.to_string());
1523        self.filename = filename.to_string();
1524        self
1525    }
1526
1527    // ── public API ───────────────────────────────────────────────
1528
1529    pub fn parse(&mut self) -> Result<Program, ParseError> {
1530        let mut program = Program {
1531            declarations: Vec::new(),
1532            declaration_trivia: Vec::new(),
1533            loc: Loc { line: 1, column: 1 },
1534        };
1535        while !self.check(TokenType::Eof) {
1536            // Capture trivia around the declaration. `start_pos` is
1537            // the effective-token position of the declaration's first
1538            // token; that position carries the leading trivia. After
1539            // parsing, `pos - 1` is the last token consumed; that
1540            // position carries the trailing trivia.
1541            let start_pos = self.pos;
1542            let mut decl = match self.parse_declaration() {
1543                Ok(d) => d,
1544                Err(e) => return Err(self.attach_source_to_error(e)),
1545            };
1546            let end_pos = self.pos.saturating_sub(1);
1547            let leading = self
1548                .leading_trivia
1549                .get(start_pos)
1550                .cloned()
1551                .unwrap_or_default();
1552            let trailing = self
1553                .trailing_trivia
1554                .get(end_pos)
1555                .cloned()
1556                .unwrap_or_default();
1557            // Fase 14.b — also copy trivia into the per-struct fields on
1558            // the declaration so consumers can read `flow.leading_trivia`
1559            // directly without going through `program.declaration_trivia[i]`.
1560            // The side-channel is preserved for backward compat with
1561            // 14.a callers and as a flat enumeration source.
1562            attach_trivia_to_decl(&mut decl, leading.clone(), trailing.clone());
1563            program.declarations.push(decl);
1564            program
1565                .declaration_trivia
1566                .push(DeclarationTrivia { leading, trailing });
1567        }
1568        // §Fase 80.g — expand `voice` declarations FIRST (they may emit
1569        // `from Preset@vN` upstream legs), then §80.f preset references,
1570        // BEFORE type-check — so the §80.c laws and the IR see the expanded
1571        // program (and `axon desugar` prints exactly this lowering).
1572        // Unknown presets stay unexpanded — the checker reports them with
1573        // the catalog list (accumulating diagnostics beat a parse abort).
1574        crate::voice_desugar::expand(&mut program);
1575        crate::upstream_presets::expand(&mut program);
1576        Ok(program)
1577    }
1578
1579    // ── §Fase 28.c — recovery-mode parse ─────────────────────────
1580    //
1581    // Mirror of Python's `Parser.parse_with_recovery` from
1582    // `axon/compiler/parser.py`. Wraps `parse_declaration` in a
1583    // try/recover loop: on any `ParseError` the error is appended to
1584    // the list and the cursor advances to the next sync point, then
1585    // parsing resumes. The two stacks must produce structurally
1586    // identical error lists on the same input — that is the cross-
1587    // stack drift gate (D7). See the test module
1588    // `tests::fase28_recovery_tests` and Python-side
1589    // `tests/test_fase28_parser_recovery.py`.
1590
1591    /// Recovery-mode parse. Collects every parse error in source
1592    /// order; the existing `parse()` API remains fail-fast (D9).
1593    ///
1594    /// # Recovery contract (D2)
1595    ///
1596    /// On `ParseError`:
1597    ///   1. Push the error onto `errors`.
1598    ///   2. If the cursor is already on a top-level declaration
1599    ///      keyword (and brace-depth ≤ 0), do not consume — the
1600    ///      caller should retry the declaration parse from here.
1601    ///      Otherwise advance one token to make progress, then
1602    ///      walk to the next sync point.
1603    ///   3. Resume the outer loop.
1604    ///
1605    /// Sync points: top-level declaration keyword at brace-depth ≤ 0,
1606    /// or EOF. Negative depths are treated identically to ≤ 0 — the
1607    /// walker keeps walking through over-balanced `}` rather than
1608    /// pretending a closing brace is itself a sync point (which would
1609    /// emit a ghost "Unexpected token at top level" error in the
1610    /// outer loop).
1611    pub fn parse_with_recovery(&mut self) -> ParseResult {
1612        let mut program = Program {
1613            declarations: Vec::new(),
1614            declaration_trivia: Vec::new(),
1615            loc: Loc { line: 1, column: 1 },
1616        };
1617        let mut errors: Vec<ParseError> = Vec::new();
1618
1619        while !self.check(TokenType::Eof) {
1620            let start_pos = self.pos;
1621            match self.parse_declaration() {
1622                Ok(mut decl) => {
1623                    let end_pos = self.pos.saturating_sub(1);
1624                    let leading = self
1625                        .leading_trivia
1626                        .get(start_pos)
1627                        .cloned()
1628                        .unwrap_or_default();
1629                    let trailing = self
1630                        .trailing_trivia
1631                        .get(end_pos)
1632                        .cloned()
1633                        .unwrap_or_default();
1634                    attach_trivia_to_decl(&mut decl, leading.clone(), trailing.clone());
1635                    program.declarations.push(decl);
1636                    program
1637                        .declaration_trivia
1638                        .push(DeclarationTrivia { leading, trailing });
1639                }
1640                Err(err) => {
1641                    // §Fase 28.d — attach source-context block when a
1642                    // source has been provided via `with_source(...)`;
1643                    // otherwise the error keeps its single-line shape.
1644                    errors.push(self.attach_source_to_error(err));
1645                    // Make progress. If parse_declaration returned
1646                    // immediately on the same token (e.g. unknown
1647                    // top-level token), we MUST advance at least one
1648                    // token to avoid an infinite loop.
1649                    if self.pos == start_pos && !self.check(TokenType::Eof) {
1650                        self.advance();
1651                    }
1652                    self.advance_to_sync_point();
1653                }
1654            }
1655        }
1656
1657        ParseResult { program, errors }
1658    }
1659
1660    /// §Fase 28.d — Decorate a `ParseError` with a `SourceSnippet`
1661    /// when the parser has source context attached, otherwise return
1662    /// the error unchanged. Idempotent: if the error already carries
1663    /// a snippet, this overwrites it with the parser's source.
1664    fn attach_source_to_error(&self, err: ParseError) -> ParseError {
1665        match &self.source {
1666            Some(src) if err.line >= 1 => err.attach_source(src, &self.filename),
1667            _ => err,
1668        }
1669    }
1670
1671    /// §Fase 28.c — Walk the cursor forward until the next sync
1672    /// point (top-level declaration keyword at brace-depth ≤ 0) or
1673    /// EOF. Used by `parse_with_recovery` to skip the malformed
1674    /// remainder of a failed declaration.
1675    fn advance_to_sync_point(&mut self) {
1676        let mut depth: i32 = 0;
1677        while !self.check(TokenType::Eof) {
1678            let tt = self.current().ttype.clone();
1679            // Sync at top-level keywords when depth ≤ 0. We do not
1680            // consume the keyword — the outer loop will dispatch on
1681            // it.
1682            if is_top_level_decl_kw_for_recovery(&tt) && depth <= 0 {
1683                return;
1684            }
1685            if matches!(tt, TokenType::LBrace) {
1686                depth += 1;
1687            } else if matches!(tt, TokenType::RBrace) {
1688                depth -= 1;
1689            }
1690            self.advance();
1691        }
1692    }
1693
1694    // ── token helpers ────────────────────────────────────────────
1695
1696    fn current(&self) -> &Token {
1697        if self.pos >= self.tokens.len() {
1698            self.tokens.last().unwrap() // EOF sentinel
1699        } else {
1700            &self.tokens[self.pos]
1701        }
1702    }
1703
1704    fn advance(&mut self) -> &Token {
1705        let idx = self.pos;
1706        if self.pos < self.tokens.len() {
1707            self.pos += 1;
1708        }
1709        &self.tokens[idx]
1710    }
1711
1712    fn check(&self, tt: TokenType) -> bool {
1713        self.current().ttype == tt
1714    }
1715
1716    fn consume(&mut self, expected: TokenType) -> Result<Token, ParseError> {
1717        let tok = self.current().clone();
1718        if tok.ttype != expected {
1719            return Err(ParseError {
1720                message: format!(
1721                    "Expected {:?}, found {:?}('{}')",
1722                    expected, tok.ttype, tok.value
1723                ),
1724                line: tok.line,
1725                column: tok.column,
1726                            ..Default::default()
1727            });
1728        }
1729        self.pos += 1;
1730        Ok(tok)
1731    }
1732
1733    /// §Fase 41.b — build a `ParseError` at the current token's location.
1734    fn error(&self, message: &str) -> ParseError {
1735        let tok = self.current();
1736        ParseError { message: message.to_string(), line: tok.line, column: tok.column, ..Default::default() }
1737    }
1738
1739    /// Consume any identifier or keyword-used-as-value.
1740    fn consume_any_ident_or_kw(&mut self) -> Result<Token, ParseError> {
1741        let tok = self.current().clone();
1742        match tok.ttype {
1743            TokenType::Identifier
1744            | TokenType::Bool
1745            | TokenType::StringLit
1746            | TokenType::Integer
1747            | TokenType::Float => {
1748                self.pos += 1;
1749                Ok(tok)
1750            }
1751            _ => {
1752                // Allow any keyword token whose value is alphabetic
1753                if !tok.value.is_empty()
1754                    && tok.value.chars().all(|c| c.is_alphanumeric() || c == '_')
1755                    && tok.ttype != TokenType::Eof
1756                {
1757                    self.pos += 1;
1758                    Ok(tok)
1759                } else {
1760                    Err(ParseError {
1761                        message: format!(
1762                            "Expected identifier or keyword value, found {:?}('{}')",
1763                            tok.ttype, tok.value
1764                        ),
1765                        line: tok.line,
1766                        column: tok.column,
1767                                            ..Default::default()
1768                    })
1769                }
1770            }
1771        }
1772    }
1773
1774    fn consume_number(&mut self) -> Result<f64, ParseError> {
1775        let tok = self.current().clone();
1776        match tok.ttype {
1777            TokenType::Float | TokenType::Integer => {
1778                self.pos += 1;
1779                tok.value.parse::<f64>().map_err(|_| ParseError {
1780                    message: format!("Invalid number '{}'", tok.value),
1781                    line: tok.line,
1782                    column: tok.column,
1783                                    ..Default::default()
1784                })
1785            }
1786            _ => Err(ParseError {
1787                message: format!("Expected number, found {:?}('{}')", tok.ttype, tok.value),
1788                line: tok.line,
1789                column: tok.column,
1790                            ..Default::default()
1791            }),
1792        }
1793    }
1794
1795    fn parse_bool(&mut self) -> Result<bool, ParseError> {
1796        let tok = self.consume(TokenType::Bool)?;
1797        Ok(tok.value == "true")
1798    }
1799
1800    fn loc_of(&self, tok: &Token) -> Loc {
1801        Loc {
1802            line: tok.line,
1803            column: tok.column,
1804        }
1805    }
1806
1807    fn check_comparison(&self) -> bool {
1808        matches!(
1809            self.current().ttype,
1810            TokenType::Lt
1811                | TokenType::Gt
1812                | TokenType::Lte
1813                | TokenType::Gte
1814                | TokenType::Eq
1815                | TokenType::Neq
1816        )
1817    }
1818
1819    fn check_run_modifier(&self) -> bool {
1820        matches!(
1821            self.current().ttype,
1822            TokenType::As
1823                | TokenType::Within
1824                | TokenType::ConstrainedBy
1825                | TokenType::OnFailure
1826                | TokenType::OutputTo
1827                | TokenType::Effort
1828        )
1829    }
1830
1831    // ── list helpers ─────────────────────────────────────────────
1832
1833    fn parse_string_list(&mut self) -> Result<Vec<String>, ParseError> {
1834        self.consume(TokenType::LBracket)?;
1835        let mut items = Vec::new();
1836        items.push(self.consume(TokenType::StringLit)?.value);
1837        while self.check(TokenType::Comma) {
1838            self.advance();
1839            items.push(self.consume(TokenType::StringLit)?.value);
1840        }
1841        self.consume(TokenType::RBracket)?;
1842        Ok(items)
1843    }
1844
1845    /// §Fase 83.a — a bracketed list of quoted string literals, tolerant of
1846    /// an empty `[]` and a trailing comma before `]` (the `Window.exclude`
1847    /// shape, generalized into a reusable helper). Used for CORS field
1848    /// lists whose values contain characters (`://`, `.`, `-`) that aren't
1849    /// valid bare identifiers — `allow_origins`, `allow_headers`,
1850    /// `expose_headers` — where `parse_string_list`'s "at least one item,
1851    /// no trailing comma" strictness would reject a legitimate empty or
1852    /// comma-terminated declaration.
1853    fn parse_bracketed_strings(&mut self) -> Result<Vec<String>, ParseError> {
1854        self.consume(TokenType::LBracket)?;
1855        let mut items = Vec::new();
1856        if !self.check(TokenType::RBracket) {
1857            items.push(self.consume(TokenType::StringLit)?.value);
1858            while self.check(TokenType::Comma) {
1859                self.advance();
1860                if self.check(TokenType::RBracket) {
1861                    break; // trailing comma
1862                }
1863                items.push(self.consume(TokenType::StringLit)?.value);
1864            }
1865        }
1866        self.consume(TokenType::RBracket)?;
1867        Ok(items)
1868    }
1869
1870    fn parse_identifier_list(&mut self) -> Result<Vec<String>, ParseError> {
1871        let mut names = Vec::new();
1872        names.push(self.consume(TokenType::Identifier)?.value);
1873        while self.check(TokenType::Comma) {
1874            self.advance();
1875            names.push(self.consume(TokenType::Identifier)?.value);
1876        }
1877        Ok(names)
1878    }
1879
1880    fn parse_bracketed_identifiers(&mut self) -> Result<Vec<String>, ParseError> {
1881        self.consume(TokenType::LBracket)?;
1882        let items = self.parse_extended_identifier_list()?;
1883        self.consume(TokenType::RBracket)?;
1884        Ok(items)
1885    }
1886
1887    fn parse_extended_identifier_list(&mut self) -> Result<Vec<String>, ParseError> {
1888        let mut items = Vec::new();
1889        items.push(self.consume_any_ident_or_kw()?.value);
1890        while self.check(TokenType::Comma) {
1891            self.advance();
1892            items.push(self.consume_any_ident_or_kw()?.value);
1893        }
1894        Ok(items)
1895    }
1896
1897    fn parse_dotted_identifier(&mut self) -> Result<String, ParseError> {
1898        let mut parts = vec![self.consume_any_ident_or_kw()?.value];
1899        while self.check(TokenType::Dot) {
1900            self.advance();
1901            parts.push(self.consume_any_ident_or_kw()?.value);
1902        }
1903        Ok(parts.join("."))
1904    }
1905
1906    fn parse_expression_string(&mut self) -> Result<String, ParseError> {
1907        if self.check(TokenType::LBracket) {
1908            let items = self.parse_bracketed_dot_identifiers()?;
1909            return Ok(format!("[{}]", items.join(", ")));
1910        }
1911        self.parse_dotted_identifier()
1912    }
1913
1914    fn parse_bracketed_dot_identifiers(&mut self) -> Result<Vec<String>, ParseError> {
1915        self.consume(TokenType::LBracket)?;
1916        let mut items = vec![self.parse_dotted_identifier()?];
1917        while self.check(TokenType::Comma) {
1918            self.advance();
1919            items.push(self.parse_dotted_identifier()?);
1920        }
1921        self.consume(TokenType::RBracket)?;
1922        Ok(items)
1923    }
1924
1925    fn parse_argument_list(&mut self) -> Result<Vec<String>, ParseError> {
1926        let mut args = Vec::new();
1927        while !self.check(TokenType::RParen) {
1928            let tok = self.current().clone();
1929            match tok.ttype {
1930                TokenType::StringLit | TokenType::Integer | TokenType::Float => {
1931                    self.advance();
1932                    args.push(tok.value);
1933                }
1934                TokenType::Identifier => {
1935                    self.advance();
1936                    let mut val = tok.value;
1937                    if self.check(TokenType::Dot) {
1938                        self.advance();
1939                        val.push('.');
1940                        val.push_str(&self.consume_any_ident_or_kw()?.value);
1941                    }
1942                    args.push(val);
1943                }
1944                _ => {
1945                    self.advance();
1946                    let key = tok.value;
1947                    if self.check(TokenType::Colon) {
1948                        self.advance();
1949                        let v = self.advance().value.clone();
1950                        args.push(format!("{key}:{v}"));
1951                    } else {
1952                        args.push(key);
1953                    }
1954                }
1955            }
1956            if self.check(TokenType::Comma) {
1957                self.advance();
1958            }
1959        }
1960        Ok(args)
1961    }
1962
1963    /// Skip a single value or balanced bracketed/braced block (unknown field).
1964    fn skip_value(&mut self) {
1965        match self.current().ttype {
1966            TokenType::LBracket => {
1967                self.advance();
1968                let mut depth = 1u32;
1969                while depth > 0 && !self.check(TokenType::Eof) {
1970                    if self.check(TokenType::LBracket) {
1971                        depth += 1;
1972                    } else if self.check(TokenType::RBracket) {
1973                        depth -= 1;
1974                    }
1975                    self.advance();
1976                }
1977            }
1978            TokenType::LBrace => {
1979                self.advance();
1980                let mut depth = 1u32;
1981                while depth > 0 && !self.check(TokenType::Eof) {
1982                    if self.check(TokenType::LBrace) {
1983                        depth += 1;
1984                    } else if self.check(TokenType::RBrace) {
1985                        depth -= 1;
1986                    }
1987                    self.advance();
1988                }
1989            }
1990            TokenType::Lt => {
1991                // effect row: <io, network, ...>
1992                self.advance();
1993                let mut depth = 1u32;
1994                while depth > 0 && !self.check(TokenType::Eof) {
1995                    if self.check(TokenType::Lt) {
1996                        depth += 1;
1997                    } else if self.check(TokenType::Gt) {
1998                        depth -= 1;
1999                    }
2000                    self.advance();
2001                }
2002            }
2003            _ => {
2004                self.advance();
2005                while self.check(TokenType::Dot) {
2006                    self.advance();
2007                    self.advance();
2008                }
2009            }
2010        }
2011    }
2012
2013    /// Skip a balanced `{ ... }` block including its braces.
2014    fn skip_braced_block(&mut self) -> Result<(), ParseError> {
2015        self.consume(TokenType::LBrace)?;
2016        let mut depth = 1u32;
2017        while depth > 0 {
2018            if self.check(TokenType::Eof) {
2019                let tok = self.current();
2020                return Err(ParseError {
2021                    message: "Unterminated block — expected '}'".to_string(),
2022                    line: tok.line,
2023                    column: tok.column,
2024                                    ..Default::default()
2025                });
2026            }
2027            if self.check(TokenType::LBrace) {
2028                depth += 1;
2029            } else if self.check(TokenType::RBrace) {
2030                depth -= 1;
2031            }
2032            self.advance();
2033        }
2034        Ok(())
2035    }
2036
2037    fn at_declaration_start(&self) -> bool {
2038        is_declaration_keyword(&self.current().ttype) || self.check(TokenType::Eof)
2039    }
2040
2041    // ── top-level dispatch ───────────────────────────────────────
2042
2043    fn parse_declaration(&mut self) -> Result<Declaration, ParseError> {
2044        let tok = self.current().clone();
2045
2046        match tok.ttype {
2047            TokenType::Import => self.parse_import().map(Declaration::Import),
2048            TokenType::Persona => self.parse_persona().map(Declaration::Persona),
2049            TokenType::Context => self.parse_context().map(Declaration::Context),
2050            TokenType::Anchor => self.parse_anchor().map(Declaration::Anchor),
2051            TokenType::Memory => self.parse_memory().map(Declaration::Memory),
2052            TokenType::Tool => self.parse_tool().map(Declaration::Tool),
2053            TokenType::Type => self.parse_type_def().map(Declaration::Type),
2054            TokenType::Flow => self.parse_flow().map(Declaration::Flow),
2055            TokenType::Intent => self.parse_intent().map(Declaration::Intent),
2056            TokenType::Run => self.parse_run().map(Declaration::Run),
2057            TokenType::Let => self.parse_let().map(Declaration::Let),
2058            TokenType::Know | TokenType::Believe | TokenType::Speculate | TokenType::Doubt => {
2059                self.parse_epistemic_block().map(Declaration::Epistemic)
2060            }
2061            TokenType::Lambda => self.parse_lambda_data().map(Declaration::LambdaData),
2062
2063            // ── Tier 2 declarations (full AST) ──────────────────
2064            TokenType::Agent => self.parse_agent().map(Declaration::Agent),
2065            TokenType::Shield => self.parse_shield().map(Declaration::Shield),
2066            // §Fase 71.a — temporal execution-window guard.
2067            TokenType::Window => self.parse_window().map(Declaration::Window),
2068            TokenType::Pix => self.parse_pix().map(Declaration::Pix),
2069            TokenType::Ledger => self.parse_ledger().map(Declaration::Ledger),
2070            TokenType::Psyche => self.parse_psyche().map(Declaration::Psyche),
2071            TokenType::Corpus => self.parse_corpus().map(Declaration::Corpus),
2072            TokenType::Dataspace => self.parse_dataspace().map(Declaration::Dataspace),
2073            TokenType::Ots => self.parse_ots().map(Declaration::Ots),
2074            TokenType::Mandate => self.parse_mandate().map(Declaration::Mandate),
2075            TokenType::Compute => self.parse_compute().map(Declaration::Compute),
2076            TokenType::Daemon => self.parse_daemon().map(Declaration::Daemon),
2077            TokenType::Extension => self.parse_extension().map(Declaration::Extension),
2078            TokenType::AxonStore => self.parse_axonstore().map(Declaration::AxonStore),
2079            TokenType::AxonEndpoint => self.parse_axonendpoint().map(Declaration::AxonEndpoint),
2080
2081            // ── §λ-L-E Fase 1 — I/O cognitivo ───────────────────
2082            TokenType::Resource => self.parse_resource().map(Declaration::Resource),
2083            TokenType::Fabric => self.parse_fabric().map(Declaration::Fabric),
2084            TokenType::Manifest => self.parse_manifest().map(Declaration::Manifest),
2085            TokenType::Observe => self.parse_observe().map(Declaration::Observe),
2086
2087            // ── §λ-L-E Fase 3 — Control cognitivo ───────────────
2088            TokenType::Reconcile => self.parse_reconcile().map(Declaration::Reconcile),
2089            TokenType::Lease => self.parse_lease().map(Declaration::Lease),
2090            TokenType::Ensemble => self.parse_ensemble().map(Declaration::Ensemble),
2091
2092            // ── §λ-L-E Fase 4 — Topology + π-calculus sessions ─
2093            TokenType::Session => self.parse_session_definition().map(Declaration::Session),
2094            TokenType::Topology => self.parse_topology().map(Declaration::Topology),
2095
2096            // ── §Fase 41.b — typed WebSocket transport ─────────
2097            TokenType::Socket => self.parse_socket().map(Declaration::Socket),
2098
2099            // ── §Fase 80.b — outbound vendor connection ─────────
2100            TokenType::Upstream => self.parse_upstream().map(Declaration::Upstream),
2101
2102            // ── §Fase 80.g — the voice-agent simplicity layer ───
2103            TokenType::Voice => self.parse_voice().map(Declaration::Voice),
2104
2105            // ── §Fase 83.a — the named origin-policy declaration ─
2106            TokenType::Cors => self.parse_cors().map(Declaration::Cors),
2107
2108            // ── §Fase 85.a — the named result-memoization policy ─
2109            TokenType::Cache => self.parse_cache().map(Declaration::Cache),
2110            TokenType::Document => self.parse_document().map(Declaration::Document),
2111
2112            // ── §Fase 105 — Governed CRM Delivery ─
2113            TokenType::Deliver => self.parse_deliver().map(Declaration::Deliver),
2114
2115            // ── §Fase 87.a — the long-horizon autonomous research primitive ─
2116            TokenType::Savant => self.parse_savant().map(Declaration::Savant),
2117
2118            // ── §Fase 87.d — the dynamic tool-synthesis policy ──────────────
2119            TokenType::Synth => self.parse_synth().map(Declaration::Synth),
2120
2121            // ── §Fase 88.a — the authorization-scope policy declaration ─────
2122            TokenType::Scope => self.parse_scope().map(Declaration::Scope),
2123
2124            // ── §Fase 92.a — the ephemeral-credential contract ──────────────
2125            TokenType::Credential => self.parse_credential().map(Declaration::Credential),
2126
2127            // ── §Fase 51.c.2 — Pauli-sum observable ────────────
2128            TokenType::Observable => self.parse_observable().map(Declaration::Observable),
2129
2130            // ── §Fase 69.a — Advantage Witness ──────────────────
2131            TokenType::Witness => self.parse_witness().map(Declaration::Witness),
2132
2133            // ── §λ-L-E Fase 5 — Cognitive immune system ─────────
2134            TokenType::Immune => self.parse_immune().map(Declaration::Immune),
2135            TokenType::Reflex => self.parse_reflex().map(Declaration::Reflex),
2136            TokenType::Heal => self.parse_heal().map(Declaration::Heal),
2137
2138            // ── §λ-L-E Fase 9 — UI cognitiva ────────────────────
2139            TokenType::Component => self.parse_component().map(Declaration::Component),
2140            TokenType::View => self.parse_view().map(Declaration::View),
2141
2142            // ── §λ-L-E Fase 13 — Mobile typed channels ──────────
2143            TokenType::Channel => self.parse_channel().map(Declaration::Channel),
2144
2145            // ── Tier 3+ structural fallback ─────────────────────
2146            // Store operations: keyword target { ... } or keyword target ...
2147            TokenType::Ingest
2148            | TokenType::Persist
2149            | TokenType::Retrieve
2150            | TokenType::Mutate
2151            | TokenType::Purge
2152            | TokenType::Transact => self.parse_generic_declaration(),
2153
2154            // MCP declaration
2155            TokenType::Mcp => self.parse_generic_declaration(),
2156
2157            _ => {
2158                // §Fase 28.e — append "Did you mean X?" hint when the
2159                // unknown token looks like a typo'd top-level keyword
2160                // (Levenshtein ≤ 2). D3, D11 ratified 2026-05-10.
2161                let hint = crate::smart_suggest::suggest_for(
2162                    &tok.value,
2163                    crate::smart_suggest::TOP_LEVEL_KEYWORD_NAMES,
2164                );
2165                let base = format!(
2166                    "Unexpected token at top level: '{}' — expected declaration \
2167                     (persona, context, anchor, flow, run, ...)",
2168                    tok.value
2169                );
2170                let message = if hint.is_empty() {
2171                    base
2172                } else {
2173                    format!("{base}. {hint}")
2174                };
2175                Err(ParseError {
2176                    message,
2177                    line: tok.line,
2178                    column: tok.column,
2179                    ..Default::default()
2180                })
2181            }
2182        }
2183    }
2184
2185    // ── IMPORT ───────────────────────────────────────────────────
2186
2187    fn parse_import(&mut self) -> Result<ImportNode, ParseError> {
2188        let tok = self.consume(TokenType::Import)?;
2189        let loc = self.loc_of(&tok);
2190
2191        let mut path_parts = Vec::new();
2192
2193        // Optional @ scope
2194        if self.check(TokenType::At) {
2195            self.advance();
2196            let first = self.consume(TokenType::Identifier)?;
2197            path_parts.push(format!("@{}", first.value));
2198        } else {
2199            let first = self.consume(TokenType::Identifier)?;
2200            path_parts.push(first.value);
2201        }
2202
2203        while self.check(TokenType::Dot) {
2204            self.advance();
2205            if self.check(TokenType::LBrace) {
2206                break;
2207            }
2208            let part = self.consume(TokenType::Identifier)?;
2209            path_parts.push(part.value);
2210        }
2211
2212        let mut names = Vec::new();
2213        if self.check(TokenType::LBrace) {
2214            self.advance();
2215            names = self.parse_identifier_list()?;
2216            self.consume(TokenType::RBrace)?;
2217        }
2218
2219        // Skip optional APX policy (with apx { ... })
2220        if self.current().value == "with" {
2221            self.advance();
2222            self.advance(); // consume "apx"
2223            if self.check(TokenType::LBrace) {
2224                self.skip_braced_block()?;
2225            }
2226        }
2227
2228        Ok(ImportNode {
2229            module_path: path_parts,
2230            names,
2231            loc,
2232            leading_trivia: Vec::new(),
2233            trailing_trivia: Vec::new(),
2234        })
2235    }
2236
2237    // ── PERSONA ──────────────────────────────────────────────────
2238
2239    fn parse_persona(&mut self) -> Result<PersonaDefinition, ParseError> {
2240        let tok = self.consume(TokenType::Persona)?;
2241        let loc = self.loc_of(&tok);
2242        let name = self.consume(TokenType::Identifier)?.value;
2243        self.consume(TokenType::LBrace)?;
2244
2245        let mut node = PersonaDefinition {
2246            name,
2247            domain: Vec::new(),
2248            tone: String::new(),
2249            confidence_threshold: None,
2250            cite_sources: None,
2251            refuse_if: Vec::new(),
2252            language: String::new(),
2253            description: String::new(),
2254            loc,
2255            leading_trivia: Vec::new(),
2256            trailing_trivia: Vec::new(),
2257        };
2258
2259        while !self.check(TokenType::RBrace) {
2260            let field_name = self.current().value.clone();
2261            self.advance();
2262            self.consume(TokenType::Colon)?;
2263
2264            match field_name.as_str() {
2265                "domain" => node.domain = self.parse_string_list()?,
2266                "tone" => node.tone = self.consume_any_ident_or_kw()?.value,
2267                "confidence_threshold" => node.confidence_threshold = Some(self.consume_number()?),
2268                "cite_sources" => node.cite_sources = Some(self.parse_bool()?),
2269                "refuse_if" => node.refuse_if = self.parse_bracketed_identifiers()?,
2270                "language" => node.language = self.consume(TokenType::StringLit)?.value,
2271                "description" => node.description = self.consume(TokenType::StringLit)?.value,
2272                _ => self.skip_value(),
2273            }
2274        }
2275        self.consume(TokenType::RBrace)?;
2276        Ok(node)
2277    }
2278
2279    // ── CONTEXT ──────────────────────────────────────────────────
2280
2281    fn parse_context(&mut self) -> Result<ContextDefinition, ParseError> {
2282        let tok = self.consume(TokenType::Context)?;
2283        let loc = self.loc_of(&tok);
2284        let name = self.consume(TokenType::Identifier)?.value;
2285        self.consume(TokenType::LBrace)?;
2286
2287        let mut node = ContextDefinition {
2288            name,
2289            memory_scope: String::new(),
2290            language: String::new(),
2291            depth: String::new(),
2292            max_tokens: None,
2293            temperature: None,
2294            cite_sources: None,
2295            now_tz: None,
2296            loc,
2297            leading_trivia: Vec::new(),
2298            trailing_trivia: Vec::new(),
2299        };
2300
2301        while !self.check(TokenType::RBrace) {
2302            let field_name = self.current().value.clone();
2303            self.advance();
2304            self.consume(TokenType::Colon)?;
2305
2306            match field_name.as_str() {
2307                "memory" => node.memory_scope = self.consume_any_ident_or_kw()?.value,
2308                "language" => node.language = self.consume(TokenType::StringLit)?.value,
2309                "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
2310                // §Fase 91.a — the frame's cognitive timezone (IANA string).
2311                "now" => node.now_tz = Some(self.consume(TokenType::StringLit)?.value),
2312                "max_tokens" => {
2313                    node.max_tokens = Some(
2314                        self.consume(TokenType::Integer)?
2315                            .value
2316                            .parse::<i64>()
2317                            .unwrap_or(0),
2318                    )
2319                }
2320                "temperature" => node.temperature = Some(self.consume_number()?),
2321                "cite_sources" => node.cite_sources = Some(self.parse_bool()?),
2322                _ => self.skip_value(),
2323            }
2324        }
2325        self.consume(TokenType::RBrace)?;
2326        Ok(node)
2327    }
2328
2329    // ── ANCHOR ───────────────────────────────────────────────────
2330
2331    fn parse_anchor(&mut self) -> Result<AnchorConstraint, ParseError> {
2332        let tok = self.consume(TokenType::Anchor)?;
2333        let loc = self.loc_of(&tok);
2334        let name = self.consume(TokenType::Identifier)?.value;
2335        self.consume(TokenType::LBrace)?;
2336
2337        let mut node = AnchorConstraint {
2338            name,
2339            require: String::new(),
2340            reject: Vec::new(),
2341            enforce: String::new(),
2342            description: String::new(),
2343            confidence_floor: None,
2344            unknown_response: String::new(),
2345            on_violation: String::new(),
2346            on_violation_target: String::new(),
2347            loc,
2348            leading_trivia: Vec::new(),
2349            trailing_trivia: Vec::new(),
2350        };
2351
2352        while !self.check(TokenType::RBrace) {
2353            let field_name = self.current().value.clone();
2354            self.advance();
2355            self.consume(TokenType::Colon)?;
2356
2357            match field_name.as_str() {
2358                "require" => node.require = self.consume_any_ident_or_kw()?.value,
2359                "description" => node.description = self.consume(TokenType::StringLit)?.value,
2360                "reject" => node.reject = self.parse_bracketed_identifiers()?,
2361                "enforce" => node.enforce = self.consume_any_ident_or_kw()?.value,
2362                "confidence_floor" => node.confidence_floor = Some(self.consume_number()?),
2363                "unknown_response" => {
2364                    node.unknown_response = self.consume(TokenType::StringLit)?.value
2365                }
2366                "on_violation" => {
2367                    // Parse: raise ErrorName | fallback(...) | identifier
2368                    let action = self.consume_any_ident_or_kw()?.value;
2369                    node.on_violation = action.clone();
2370                    if action == "raise" || action == "fallback" {
2371                        node.on_violation_target = self.consume_any_ident_or_kw()?.value;
2372                    }
2373                }
2374                _ => self.skip_value(),
2375            }
2376        }
2377        self.consume(TokenType::RBrace)?;
2378        Ok(node)
2379    }
2380
2381    // ── MEMORY ───────────────────────────────────────────────────
2382
2383    fn parse_memory(&mut self) -> Result<MemoryDefinition, ParseError> {
2384        let tok = self.consume(TokenType::Memory)?;
2385        let loc = self.loc_of(&tok);
2386        let name = self.consume(TokenType::Identifier)?.value;
2387        self.consume(TokenType::LBrace)?;
2388
2389        let mut node = MemoryDefinition {
2390            name,
2391            store: String::new(),
2392            backend: String::new(),
2393            retrieval: String::new(),
2394            decay: String::new(),
2395            loc,
2396            leading_trivia: Vec::new(),
2397            trailing_trivia: Vec::new(),
2398        };
2399
2400        while !self.check(TokenType::RBrace) {
2401            let field_name = self.current().value.clone();
2402            self.advance();
2403            self.consume(TokenType::Colon)?;
2404
2405            match field_name.as_str() {
2406                "store" => node.store = self.consume_any_ident_or_kw()?.value,
2407                "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
2408                "retrieval" => node.retrieval = self.consume_any_ident_or_kw()?.value,
2409                "decay" => {
2410                    if self.check(TokenType::Duration) {
2411                        node.decay = self.advance().value.clone();
2412                    } else {
2413                        node.decay = self.consume_any_ident_or_kw()?.value;
2414                    }
2415                }
2416                _ => self.skip_value(),
2417            }
2418        }
2419        self.consume(TokenType::RBrace)?;
2420        Ok(node)
2421    }
2422
2423    // ── TOOL ─────────────────────────────────────────────────────
2424
2425    fn parse_tool(&mut self) -> Result<ToolDefinition, ParseError> {
2426        let tok = self.consume(TokenType::Tool)?;
2427        let loc = self.loc_of(&tok);
2428        let name = self.consume(TokenType::Identifier)?.value;
2429        self.consume(TokenType::LBrace)?;
2430
2431        let mut node = ToolDefinition {
2432            name,
2433            provider: String::new(),
2434            max_results: None,
2435            filter_expr: String::new(),
2436            timeout: String::new(),
2437            runtime: String::new(),
2438            sandbox: None,
2439            effects: None,
2440            parameters: Vec::new(),
2441            output_type: None,
2442            secret: String::new(),
2443            secret_partition: String::new(),
2444            target: None,
2445            risk: None,
2446            argv: Vec::new(),
2447            cache: String::new(),
2448            scrape: None,
2449            loc,
2450            leading_trivia: Vec::new(),
2451            trailing_trivia: Vec::new(),
2452        };
2453
2454        // §Fase 84.b/D84.13 — unknown fields are recorded (not silently
2455        // skipped) so a `target:`-bound technician tool can HARD-ERROR on one
2456        // (a typo'd safety field must never quietly disable a guard), while a
2457        // legacy schema-less tool keeps its lenient record-and-skip (zero
2458        // regression). The decision is deferred to after the block is parsed,
2459        // since `target:` may appear after the unknown field.
2460        let mut unknown_fields: Vec<(String, u32, u32)> = Vec::new();
2461
2462        while !self.check(TokenType::RBrace) {
2463            let field_tok = self.current().clone();
2464            let field_name = field_tok.value.clone();
2465            self.advance();
2466            self.consume(TokenType::Colon)?;
2467
2468            match field_name.as_str() {
2469                "provider" => node.provider = self.consume_any_ident_or_kw()?.value,
2470                "max_results" => {
2471                    node.max_results = Some(
2472                        self.consume(TokenType::Integer)?
2473                            .value
2474                            .parse::<i64>()
2475                            .unwrap_or(0),
2476                    )
2477                }
2478                "filter" => node.filter_expr = self.parse_filter_expression()?,
2479                "timeout" => node.timeout = self.consume(TokenType::Duration)?.value,
2480                "runtime" => node.runtime = self.consume_any_ident_or_kw()?.value,
2481                "sandbox" => node.sandbox = Some(self.parse_bool()?),
2482                "effects" => node.effects = Some(self.parse_effect_row()?),
2483                // §Fase 58.a — the tool's typed input schema + output type.
2484                "parameters" => node.parameters = self.parse_tool_param_schema()?,
2485                "output_type" => node.output_type = Some(self.parse_output_type_string()?),
2486                // §Fase 94.c — the per-tenant secret KEY injected at
2487                // dispatch (`rotation_without_revelation`). Key shape +
2488                // technician exclusion are `axon-T902` (type-checker).
2489                "secret" => node.secret = self.parse_dotted_identifier()?,
2490                // §Fase 95.a — `secret_partition:` names one of this tool's
2491                // own `parameters:` (a bare identifier, NOT dotted — it is a
2492                // parameter reference, not a key). Its runtime value becomes
2493                // a single appended key segment at dispatch. The membership +
2494                // `String`-type + technician laws are `axon-T903`.
2495                "secret_partition" => {
2496                    node.secret_partition = self.consume_any_ident_or_kw()?.value
2497                }
2498                // §Fase 84.b — Remote Hands technician fields.
2499                "target" => node.target = Some(self.consume_any_ident_or_kw()?.value),
2500                "risk" => node.risk = Some(self.consume_any_ident_or_kw()?.value),
2501                // The argv template: a bracketed list of quoted elements
2502                // (`argv: ["ping", "-c", "${count}", "${host}"]`). Reuses the
2503                // CORS list helper (tolerant of `[]` and a trailing comma).
2504                "argv" => node.argv = self.parse_bracketed_strings()?,
2505                // §Fase 85.b — the tool's result-memoization policy reference
2506                // (a declared `cache` name, or the `none` opt-out sentinel).
2507                "cache" => node.cache = self.consume_any_ident_or_kw()?.value,
2508                // §Fase 98.b — the closed-catalog web-acquisition config
2509                // block. `scrape: { engine: …, extract: […], … }`.
2510                "scrape" => node.scrape = Some(self.parse_scrape_spec()?),
2511                _ => {
2512                    unknown_fields.push((field_name, field_tok.line, field_tok.column));
2513                    self.skip_value();
2514                }
2515            }
2516        }
2517        self.consume(TokenType::RBrace)?;
2518
2519        // §Fase 84.b/D84.13 — a `target:`-bound tool opts into strict field
2520        // checking. An unknown field on it is a parse error, mirroring the §83
2521        // `cors`/`voice` closed-catalog discipline — but scoped to the
2522        // technician surface so ordinary tools are untouched.
2523        // §Fase 98.b (D98.2) — a `scrape:`-bearing web-acquisition tool opts
2524        // into the same strictness: a typo'd safety field (e.g. a mis-spelled
2525        // `respect_robots`) must never quietly disable a guard.
2526        if node.target.is_some() || node.scrape.is_some() {
2527            if let Some((field_name, line, column)) = unknown_fields.into_iter().next() {
2528                let (surface, valid) = if node.target.is_some() {
2529                    (
2530                        "technician tool (§Fase 84 D84.13)",
2531                        "provider, parameters, output_type, timeout, effects, target, risk, argv",
2532                    )
2533                } else {
2534                    (
2535                        "web-acquisition tool (§Fase 98 D98.2)",
2536                        "provider, parameters, output_type, timeout, effects, secret, \
2537                         secret_partition, cache, scrape",
2538                    )
2539                };
2540                return Err(ParseError {
2541                    message: format!(
2542                        "unknown field `{field_name}` in {surface} `{}` — this tool uses \
2543                         strict field checking; valid fields: {valid}",
2544                        node.name
2545                    ),
2546                    line,
2547                    column,
2548                    ..Default::default()
2549                });
2550            }
2551        }
2552        Ok(node)
2553    }
2554
2555    /// §Fase 98.b — parse the closed-catalog `scrape: { … }` web-acquisition
2556    /// config sub-block. Every field is optional; an unknown field is a hard
2557    /// parse error (the §83 `cors` closed-catalog discipline). Mirrors the
2558    /// field grammar of `parse_tool` for the scrape-specific keys.
2559    fn parse_scrape_spec(&mut self) -> Result<crate::ast::ScrapeSpec, ParseError> {
2560        let open = self.consume(TokenType::LBrace)?;
2561        let loc = self.loc_of(&open);
2562        let mut spec = crate::ast::ScrapeSpec {
2563            loc,
2564            ..Default::default()
2565        };
2566        while !self.check(TokenType::RBrace) {
2567            let field_tok = self.current().clone();
2568            let field_name = field_tok.value.clone();
2569            self.advance();
2570            self.consume(TokenType::Colon)?;
2571            match field_name.as_str() {
2572                "engine" => spec.engine = Some(self.consume_any_ident_or_kw()?.value),
2573                "impersonate" => spec.impersonate = Some(self.consume_any_ident_or_kw()?.value),
2574                "render_wait" => spec.render_wait = Some(self.consume(TokenType::Duration)?.value),
2575                "proxy" => spec.proxy = self.parse_dotted_identifier()?,
2576                "respect_robots" => spec.respect_robots = Some(self.parse_bool()?),
2577                "extract" => spec.extract = self.parse_bracketed_strings()?,
2578                "adaptive" => spec.adaptive = Some(self.parse_bool()?),
2579                "similarity_floor" => spec.similarity_floor = self.parse_optional_float(),
2580                "follow" => spec.follow = self.consume(TokenType::StringLit)?.value,
2581                "max_depth" => {
2582                    spec.max_depth =
2583                        Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2584                }
2585                "max_pages" => {
2586                    spec.max_pages =
2587                        Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2588                }
2589                "concurrency" => {
2590                    spec.concurrency =
2591                        Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2592                }
2593                "politeness" => spec.politeness = self.consume_any_ident_or_kw()?.value,
2594                "checkpoint" => spec.checkpoint = self.consume_any_ident_or_kw()?.value,
2595                other => {
2596                    return Err(self.error(&format!(
2597                        "unknown scrape field `{other}` — the `scrape: {{ … }}` block is a \
2598                         closed catalog (§Fase 98 D98.2); valid fields: engine, impersonate, \
2599                         render_wait, proxy, respect_robots, extract, adaptive, \
2600                         similarity_floor, follow, max_depth, max_pages, concurrency, \
2601                         politeness, checkpoint"
2602                    )));
2603                }
2604            }
2605        }
2606        self.consume(TokenType::RBrace)?;
2607        Ok(spec)
2608    }
2609
2610    /// §Fase 58.a — parse a tool's INPUT SCHEMA: a brace-delimited list of
2611    /// `name: Type` parameters (`parameters: { query: String, max_results: Int }`).
2612    /// Reuses the flow-parameter shape (`Parameter`), so the same `TypeExpr`
2613    /// grammar — generics like `List<T>`, `?`-optionals — applies. A trailing
2614    /// comma is tolerated; an empty `{}` yields no parameters.
2615    fn parse_tool_param_schema(&mut self) -> Result<Vec<Parameter>, ParseError> {
2616        self.consume(TokenType::LBrace)?;
2617        let mut params = Vec::new();
2618        while !self.check(TokenType::RBrace) {
2619            // Accept a keyword-as-name (`filter`, `type`, `domain`, …) — real
2620            // adopter tool schemas use such parameter names; the `:` after it
2621            // disambiguates.
2622            let name = self.consume_any_ident_or_kw()?;
2623            let ploc = self.loc_of(&name);
2624            self.consume(TokenType::Colon)?;
2625            let type_expr = self.parse_type_expr()?;
2626            params.push(Parameter {
2627                name: name.value,
2628                type_expr,
2629                loc: ploc,
2630            });
2631            if self.check(TokenType::Comma) {
2632                self.advance();
2633            } else {
2634                break;
2635            }
2636        }
2637        self.consume(TokenType::RBrace)?;
2638        Ok(params)
2639    }
2640
2641    fn parse_filter_expression(&mut self) -> Result<String, ParseError> {
2642        let name = self.consume_any_ident_or_kw()?.value;
2643        if self.check(TokenType::LParen) {
2644            self.advance();
2645            let mut parts = vec![name, "(".to_string()];
2646            while !self.check(TokenType::RParen) {
2647                parts.push(self.advance().value.clone());
2648            }
2649            self.consume(TokenType::RParen)?;
2650            parts.push(")".to_string());
2651            Ok(parts.join(""))
2652        } else {
2653            Ok(name)
2654        }
2655    }
2656
2657    fn parse_effect_row(&mut self) -> Result<EffectRow, ParseError> {
2658        let tok = self.consume(TokenType::Lt)?;
2659        let loc = self.loc_of(&tok);
2660        let mut effects = Vec::new();
2661        let mut epistemic_level = String::new();
2662
2663        while !self.check(TokenType::Gt) {
2664            let name = self.consume_any_ident_or_kw()?.value;
2665            if self.check(TokenType::Colon) {
2666                self.advance();
2667                // Fase 11.c / 11.e — qualifiers can be compound slugs
2668                // from a closed catalogue:
2669                //
2670                //   * dot-separated  — `legal:HIPAA.164_502`,
2671                //                       `legal:GDPR.Art6.Consent`,
2672                //                       `legal:PCI_DSS.v4_Req3`
2673                //   * colon-separated — `ots:transform:mulaw8:pcm16`,
2674                //                       `ots:backend:native`
2675                //   * mixed           — supported by the same loop.
2676                //
2677                // The lexer fragments dotted slugs across IDENT /
2678                // INTEGER tokens (e.g., `164_502` lexes as INTEGER
2679                // `164` + IDENT `_502` because `_` starts a fresh
2680                // identifier); we recombine here using source-column
2681                // adjacency so the type checker sees the catalog
2682                // string verbatim.
2683                let level = self.parse_qualifier_value()?;
2684                if name == "epistemic" {
2685                    epistemic_level = level;
2686                } else {
2687                    effects.push(format!("{name}:{level}"));
2688                }
2689            } else {
2690                effects.push(name);
2691            }
2692            if self.check(TokenType::Comma) {
2693                self.advance();
2694            }
2695        }
2696        self.consume(TokenType::Gt)?;
2697
2698        Ok(EffectRow {
2699            effects,
2700            epistemic_level,
2701            loc,
2702        })
2703    }
2704
2705    /// Parse a compound qualifier value following an effect's first
2706    /// colon — supports both dot-separated (`HIPAA.164_502`) and
2707    /// colon-separated (`transform:mulaw8:pcm16`) catalogue slugs, as
2708    /// well as mixed forms.
2709    ///
2710    /// The grammar is: `segment ((`.` | `:`) segment)*` where a
2711    /// segment is a contiguous run of IDENT / INTEGER tokens (see
2712    /// [`Self::consume_dotted_slug_segment`]).
2713    fn parse_qualifier_value(&mut self) -> Result<String, ParseError> {
2714        let mut buf = self.consume_dotted_slug_segment()?;
2715        loop {
2716            let sep = if self.check(TokenType::Dot) {
2717                '.'
2718            } else if self.check(TokenType::Colon) {
2719                ':'
2720            } else {
2721                break;
2722            };
2723            self.advance();
2724            let part = self.consume_dotted_slug_segment()?;
2725            buf.push(sep);
2726            buf.push_str(&part);
2727        }
2728        Ok(buf)
2729    }
2730
2731    /// Consume a contiguous run of IDENT / INTEGER / keyword-ident
2732    /// tokens whose source positions are adjacent (no whitespace
2733    /// between them), concatenating their text into a single segment.
2734    ///
2735    /// Needed for closed-catalogue qualifier slugs whose segment
2736    /// mixes digits and identifier characters — e.g. `HIPAA.164_502`
2737    /// lexes as INTEGER `164` + IDENT `_502` because `_` starts a
2738    /// fresh identifier; the catalog value is the concatenation
2739    /// `164_502`. Adjacency is determined by matching
2740    /// `(line, column + len)` of the previous token against the next
2741    /// token's start position.
2742    fn consume_dotted_slug_segment(&mut self) -> Result<String, ParseError> {
2743        let first = self.consume_any_ident_or_kw()?;
2744        let mut buf = first.value.clone();
2745        let mut next_line = first.line;
2746        let mut next_col = first.column + first.value.chars().count() as u32;
2747        loop {
2748            let cur = self.current();
2749            let is_segment_token = matches!(cur.ttype, TokenType::Identifier | TokenType::Integer,);
2750            if !is_segment_token {
2751                break;
2752            }
2753            if cur.line != next_line || cur.column != next_col {
2754                break;
2755            }
2756            buf.push_str(&cur.value);
2757            next_col = cur.column + cur.value.chars().count() as u32;
2758            next_line = cur.line;
2759            self.pos += 1;
2760        }
2761        Ok(buf)
2762    }
2763
2764    // ── TYPE ─────────────────────────────────────────────────────
2765
2766    fn parse_type_def(&mut self) -> Result<TypeDefinition, ParseError> {
2767        let tok = self.consume(TokenType::Type)?;
2768        let loc = self.loc_of(&tok);
2769        let name = self.consume(TokenType::Identifier)?.value;
2770
2771        let mut node = TypeDefinition {
2772            name,
2773            fields: Vec::new(),
2774            range_constraint: None,
2775            where_clause: None,
2776            compliance: Vec::new(),
2777            loc: loc.clone(),
2778            leading_trivia: Vec::new(),
2779            trailing_trivia: Vec::new(),
2780        };
2781
2782        // Optional range: (0.0..1.0)
2783        if self.check(TokenType::LParen) {
2784            self.advance();
2785            let min_val = self.consume_number()?;
2786            self.consume(TokenType::DotDot)?;
2787            let max_val = self.consume_number()?;
2788            self.consume(TokenType::RParen)?;
2789            node.range_constraint = Some(RangeConstraint {
2790                min_value: min_val,
2791                max_value: max_val,
2792                loc: loc.clone(),
2793            });
2794        }
2795
2796        // Optional where clause
2797        if self.check(TokenType::Where) {
2798            self.advance();
2799            let mut expr_parts = Vec::new();
2800            while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
2801                if self.check(TokenType::Eof) {
2802                    break;
2803                }
2804                expr_parts.push(self.advance().value.clone());
2805            }
2806            node.where_clause = Some(WhereClause {
2807                expression: expr_parts.join(" "),
2808                loc: loc.clone(),
2809            });
2810        }
2811
2812        // Optional ESK Fase 6.1 — `compliance [HIPAA, ...]` prefix modifier
2813        // between `type Name` / `range` / `where` and the body `{`.
2814        if self.check(TokenType::Identifier) && self.current().value == "compliance" {
2815            self.advance();
2816            node.compliance = self.parse_bracketed_identifiers()?;
2817        }
2818
2819        // Optional body: { field: Type, ... }
2820        if self.check(TokenType::LBrace) {
2821            self.advance();
2822            while !self.check(TokenType::RBrace) {
2823                let field_name = self.consume(TokenType::Identifier)?;
2824                let field_loc = self.loc_of(&field_name);
2825                self.consume(TokenType::Colon)?;
2826                let type_expr = self.parse_type_expr()?;
2827                node.fields.push(TypeField {
2828                    name: field_name.value,
2829                    type_expr,
2830                    loc: field_loc,
2831                });
2832                if self.check(TokenType::Comma) {
2833                    self.advance();
2834                }
2835            }
2836            self.consume(TokenType::RBrace)?;
2837        }
2838
2839        Ok(node)
2840    }
2841
2842    fn parse_type_expr(&mut self) -> Result<TypeExpr, ParseError> {
2843        let name_tok = self.consume(TokenType::Identifier)?;
2844        let loc = self.loc_of(&name_tok);
2845        let mut generic_param = String::new();
2846        let mut optional = false;
2847
2848        if self.check(TokenType::Lt) {
2849            self.advance();
2850            // §Fase 39.a — recursive: the generic param can itself be a
2851            // nested type expression. `FlowEnvelope<List<TenantRecord>>`
2852            // parses as outer=FlowEnvelope, inner=List<TenantRecord>.
2853            // Pre-39.a the inner had to be a single Identifier; nested
2854            // generics like the canonical FlowEnvelope<T> wrapper
2855            // required this lift. Backwards-compat preserved for
2856            // single-level generics like `Stream<Token>` and
2857            // `List<T>` — the recursion lands once and returns the
2858            // same flat string the v1.x parser produced.
2859            let inner = self.parse_type_expr()?;
2860            generic_param = if inner.generic_param.is_empty() {
2861                inner.name
2862            } else {
2863                format!("{}<{}>", inner.name, inner.generic_param)
2864            };
2865            self.consume(TokenType::Gt)?;
2866        }
2867        // §Fase 51.c.3 — bracket type parameters for the continuous-carrier
2868        // grammar: `SymbolicPtr[Tensor[Float32]]`, `DensityMatrix[1024]`. The
2869        // param is either a nested type expression OR a numeric dimension.
2870        if self.check(TokenType::LBracket) {
2871            self.advance();
2872            if matches!(self.current().ttype, TokenType::Integer | TokenType::Float) {
2873                generic_param = self.advance().value.clone();
2874            } else {
2875                let inner = self.parse_type_expr()?;
2876                generic_param = if inner.generic_param.is_empty() {
2877                    inner.name
2878                } else {
2879                    format!("{}[{}]", inner.name, inner.generic_param)
2880                };
2881            }
2882            self.consume(TokenType::RBracket)?;
2883        }
2884        if self.check(TokenType::Question) {
2885            self.advance();
2886            optional = true;
2887        }
2888
2889        Ok(TypeExpr {
2890            name: name_tok.value,
2891            generic_param,
2892            optional,
2893            loc,
2894        })
2895    }
2896
2897    /// Parse a type expression in a context where the AST stores the
2898    /// shape as a flat string (step / reason / forge / ots-apply
2899    /// productions). Mirrors Python `_parse_output_type_string`.
2900    ///
2901    /// Accepts:
2902    /// - `Identifier`        → `"Identifier"`
2903    /// - `Stream<String>`    → `"Stream<String>"`
2904    /// - `Optional?`         → `"Optional?"`
2905    /// - `Stream<String>?`   → `"Stream<String>?"`
2906    ///
2907    /// **Why this exists** — pre-fix, the step parser called
2908    /// `consume(TokenType::Identifier)?.value` which captured only
2909    /// the head identifier and left `< … >` unconsumed. For
2910    /// `output: Stream<Token>`, this produced `output_type =
2911    /// "Stream"`, and downstream `flow_has_stream_output`'s
2912    /// `starts_with("Stream<") && ends_with('>')` predicate then
2913    /// returned false → `implicit_transport == "json"` → the
2914    /// dynamic-route fallback in `axon-rs` served JSON instead of
2915    /// SSE even when the adopter's source canonically declared the
2916    /// algebraic stream effect. Surfaced 2026-05-12 by adopter
2917    /// `docs/MIGRATION_TO_AXON.md` audit after the v1.23.0 wire-
2918    /// layer didn't honor the declarative effect. Python parser was
2919    /// fixed for the same gap 2026-05-09; this is the Rust cross-
2920    /// stack catch-up.
2921    fn parse_output_type_string(&mut self) -> Result<String, ParseError> {
2922        let expr = self.parse_type_expr()?;
2923        let mut s = expr.name;
2924        if !expr.generic_param.is_empty() {
2925            s.push('<');
2926            s.push_str(&expr.generic_param);
2927            s.push('>');
2928        }
2929        if expr.optional {
2930            s.push('?');
2931        }
2932        Ok(s)
2933    }
2934
2935    // ── FLOW ─────────────────────────────────────────────────────
2936
2937    fn parse_flow(&mut self) -> Result<FlowDefinition, ParseError> {
2938        let tok = self.consume(TokenType::Flow)?;
2939        let loc = self.loc_of(&tok);
2940        let name = self.consume(TokenType::Identifier)?.value;
2941
2942        self.consume(TokenType::LParen)?;
2943        let mut parameters = Vec::new();
2944        if !self.check(TokenType::RParen) {
2945            parameters = self.parse_param_list()?;
2946        }
2947        self.consume(TokenType::RParen)?;
2948
2949        let mut return_type = None;
2950        if self.check(TokenType::Arrow) {
2951            self.advance();
2952            return_type = Some(self.parse_type_expr()?);
2953        }
2954
2955        self.consume(TokenType::LBrace)?;
2956        let mut body = Vec::new();
2957        while !self.check(TokenType::RBrace) {
2958            body.push(self.parse_flow_step()?);
2959        }
2960        self.consume(TokenType::RBrace)?;
2961
2962        Ok(FlowDefinition {
2963            name,
2964            parameters,
2965            return_type,
2966            body,
2967            loc,
2968            leading_trivia: Vec::new(),
2969            trailing_trivia: Vec::new(),
2970        })
2971    }
2972
2973    fn parse_param_list(&mut self) -> Result<Vec<Parameter>, ParseError> {
2974        let mut params = Vec::new();
2975
2976        let name = self.consume(TokenType::Identifier)?;
2977        let ploc = self.loc_of(&name);
2978        self.consume(TokenType::Colon)?;
2979        let type_expr = self.parse_type_expr()?;
2980        params.push(Parameter {
2981            name: name.value,
2982            type_expr,
2983            loc: ploc,
2984        });
2985
2986        while self.check(TokenType::Comma) {
2987            self.advance();
2988            let name = self.consume(TokenType::Identifier)?;
2989            let ploc = self.loc_of(&name);
2990            self.consume(TokenType::Colon)?;
2991            let type_expr = self.parse_type_expr()?;
2992            params.push(Parameter {
2993                name: name.value,
2994                type_expr,
2995                loc: ploc,
2996            });
2997        }
2998        Ok(params)
2999    }
3000
3001    // ── FLOW STEP dispatch ───────────────────────────────────────
3002
3003    fn parse_flow_step(&mut self) -> Result<FlowStep, ParseError> {
3004        let tok = self.current().clone();
3005
3006        match tok.ttype {
3007            TokenType::Step => self.parse_step().map(FlowStep::Step),
3008            TokenType::If => self.parse_if().map(FlowStep::If),
3009            TokenType::For => self.parse_for_in().map(FlowStep::ForIn),
3010            TokenType::Let => self.parse_let().map(FlowStep::Let),
3011            TokenType::Return => self.parse_return().map(FlowStep::Return),
3012            TokenType::Break => self.parse_break().map(FlowStep::Break),
3013            TokenType::Continue => self.parse_continue().map(FlowStep::Continue),
3014            TokenType::Lambda => self.parse_lambda_data_apply().map(FlowStep::LambdaDataApply),
3015
3016            // ── Tier 2 flow steps (typed AST) ─────────────────────
3017            TokenType::Probe => self.parse_flow_step_simple("probe").map(|l| FlowStep::Probe(ProbeStep { target: l.1, loc: l.0 })),
3018            TokenType::Reason => self.parse_flow_step_simple("reason").map(|l| FlowStep::Reason(ReasonStep { strategy: String::new(), target: l.1, loc: l.0 })),
3019            TokenType::Validate => self.parse_flow_step_simple("validate").map(|l| FlowStep::Validate(ValidateStep { target: l.1, rule: String::new(), loc: l.0 })),
3020            TokenType::Refine => self.parse_flow_step_simple("refine").map(|l| FlowStep::Refine(RefineStep { target: l.1, strategy: String::new(), loc: l.0 })),
3021            TokenType::Weave => self.parse_weave_step(),
3022            TokenType::Use => self.parse_use_step(),
3023            TokenType::Remember => self.parse_remember_step(),
3024            TokenType::Recall => self.parse_recall_step(),
3025            TokenType::Par => self.parse_par_block().map(FlowStep::Par),
3026            TokenType::Hibernate => self.parse_hibernate_step(),
3027            TokenType::Deliberate => self.parse_block_step("deliberate").map(|l| FlowStep::Deliberate(DeliberateBlock { loc: l })),
3028            TokenType::Consensus => self.parse_block_step("consensus").map(|l| FlowStep::Consensus(ConsensusBlock { loc: l })),
3029            TokenType::Forge => self.parse_forge_step().map(FlowStep::Forge),
3030            TokenType::Focus => self.parse_focus_step(),
3031            TokenType::Grad => self.parse_grad_step(),
3032            TokenType::Associate => self.parse_associate_step(),
3033            TokenType::Aggregate => self.parse_aggregate_step(),
3034            TokenType::Explore => self.parse_explore_step(),
3035            TokenType::Ingest => self.parse_ingest_step(),
3036            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 })),
3037            TokenType::Stream => self.parse_block_step("stream").map(|l| FlowStep::Stream(StreamBlock { loc: l })),
3038            TokenType::Navigate => self.parse_navigate_step(),
3039            TokenType::Drill => self.parse_drill_step(),
3040            TokenType::Trail => self.parse_flow_step_simple("trail").map(|l| FlowStep::Trail(TrailStep { navigate_ref: l.1, loc: l.0 })),
3041            TokenType::Corroborate => self.parse_corroborate_step(),
3042            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 })),
3043            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 })),
3044            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 })),
3045            TokenType::Listen => self.parse_listen_step(),
3046            TokenType::Daemon => self.parse_flow_step_simple("daemon").map(|l| FlowStep::DaemonStep(DaemonStepNode { daemon_ref: l.1, loc: l.0 })),
3047            // §λ-L-E Fase 13 — Mobile typed channels (paper §3.1, §3.2, §4.3)
3048            TokenType::Emit => self.parse_emit_step(),
3049            // §Fase 92.b — `mint <Credential> as <binding>` (ephemeral credential).
3050            TokenType::Mint => self.parse_mint_step(),
3051            // §Fase 94.b — `rotate <SecretsStore> [where "…"] with <Tool> as
3052            // <binding>` (mediated secret renewal).
3053            TokenType::Rotate => self.parse_rotate_step(),
3054            TokenType::Publish => self.parse_publish_step(),
3055            TokenType::Discover => self.parse_discover_step(),
3056            TokenType::Persist => self.parse_persist_step(),
3057            TokenType::Retrieve => self.parse_retrieve_step(),
3058            TokenType::Mutate => self.parse_mutate_step(),
3059            TokenType::Purge => self.parse_store_where_step().map(|(loc, store_name, where_expr)| FlowStep::Purge(PurgeStep { store_name, where_expr, loc })),
3060            TokenType::Transact => self.parse_block_step("transact").map(|l| FlowStep::Transact(TransactBlock { loc: l })),
3061            // §Fase 88.a — the `warden` adversarial-analysis block.
3062            TokenType::Warden => self.parse_warden().map(FlowStep::Warden),
3063            // §Fase 51.a — the `quant` cognitive block (Hilbert-space projection).
3064            TokenType::Quant => self.parse_quant().map(FlowStep::Quant),
3065            // §Fase 51.d.2 — the `yield` measurement point.
3066            TokenType::Yield => self.parse_yield().map(FlowStep::Yield),
3067            // §Fase 52.c — `run <Flow>(args)` as a flow-step: invoke a declared
3068            // flow from inside a body (a `daemon` listen handler, Q3). Reuses
3069            // the top-level run parser.
3070            TokenType::Run => self.parse_run().map(FlowStep::Run),
3071
3072            _ => {
3073                // §Fase 28.e — append "Did you mean X?" hint when the
3074                // unknown token looks like a typo'd flow-body keyword
3075                // (e.g. `stepp` / `reasn` / `validte`). D3, D11.
3076                let hint = crate::smart_suggest::suggest_for(
3077                    &tok.value,
3078                    crate::smart_suggest::FLOW_BODY_KEYWORD_NAMES,
3079                );
3080                let base = format!(
3081                    "Unexpected token in flow body: '{}' — expected step, if, for, let, return, ...",
3082                    tok.value
3083                );
3084                let message = if hint.is_empty() {
3085                    base
3086                } else {
3087                    format!("{base}. {hint}")
3088                };
3089                Err(ParseError {
3090                    message,
3091                    line: tok.line,
3092                    column: tok.column,
3093                    ..Default::default()
3094                })
3095            }
3096        }
3097    }
3098
3099    // ── STEP ─────────────────────────────────────────────────────
3100
3101    fn parse_step(&mut self) -> Result<StepNode, ParseError> {
3102        let tok = self.consume(TokenType::Step)?;
3103        let loc = self.loc_of(&tok);
3104        let name = self.consume(TokenType::Identifier)?.value;
3105
3106        let mut persona_ref = String::new();
3107        if self.check(TokenType::Use) {
3108            self.advance();
3109            persona_ref = self.consume_any_ident_or_kw()?.value;
3110        }
3111
3112        self.consume(TokenType::LBrace)?;
3113
3114        let mut node = StepNode {
3115            name,
3116            persona_ref,
3117            given: String::new(),
3118            ask: String::new(),
3119            output_type: String::new(),
3120            confidence_floor: None,
3121            navigate_ref: String::new(),
3122            apply_ref: String::new(),
3123            requires_context: None,
3124            now_tz: None,
3125            loc,
3126        };
3127
3128        while !self.check(TokenType::RBrace) {
3129            let inner = self.current().clone();
3130
3131            match inner.ttype {
3132                TokenType::Given => {
3133                    self.advance();
3134                    self.consume(TokenType::Colon)?;
3135                    node.given = self.parse_expression_string()?;
3136                }
3137                TokenType::Ask => {
3138                    self.advance();
3139                    self.consume(TokenType::Colon)?;
3140                    node.ask = self.consume(TokenType::StringLit)?.value;
3141                }
3142                TokenType::Output => {
3143                    // Mirror of Python `_parse_step` `case "output":`
3144                    // which uses `_parse_output_type_string` — accepts
3145                    // the FULL generic-aware shape `Stream<T>`,
3146                    // `Stream<T>?`, `Identifier?`, NOT just the bare
3147                    // head identifier. Pre-fix the step parser dropped
3148                    // `<T>` and downstream `flow_has_stream_output`'s
3149                    // `starts_with("Stream<") && ends_with('>')` then
3150                    // returned false → `implicit_transport == "json"`
3151                    // → dynamic routes served JSON instead of SSE.
3152                    self.advance();
3153                    self.consume(TokenType::Colon)?;
3154                    node.output_type = self.parse_output_type_string()?;
3155                }
3156                TokenType::Navigate => {
3157                    self.advance();
3158                    self.consume(TokenType::Colon)?;
3159                    node.navigate_ref = self.parse_dotted_identifier()?;
3160                }
3161                TokenType::Identifier if inner.value == "confidence_floor" => {
3162                    self.advance();
3163                    self.consume(TokenType::Colon)?;
3164                    node.confidence_floor = Some(self.consume_number()?);
3165                }
3166                TokenType::Identifier if inner.value == "apply" => {
3167                    self.advance();
3168                    self.consume(TokenType::Colon)?;
3169                    node.apply_ref = self.consume_any_ident_or_kw()?.value;
3170                }
3171                // §Fase 68.b — `requires_context: <tokens>`: the step's declared
3172                // model-capability requirement (the context window the cognition
3173                // needs). A bare positive integer literal; the §68.c resolver maps
3174                // it to a concrete model. Range/ceiling is the type-checker's job
3175                // (§68.b positive-int + §68.f catalog ceiling) — the parser only
3176                // requires an integer token here (a float / non-number is a parse
3177                // error, surfaced at the exact column).
3178                TokenType::Identifier if inner.value == "requires_context" => {
3179                    self.advance();
3180                    self.consume(TokenType::Colon)?;
3181                    let num = self.current().clone();
3182                    let bad = |tok: &crate::tokens::Token| ParseError {
3183                        message: format!(
3184                            "`requires_context:` must be a positive integer token count \
3185                             (got '{}')",
3186                            tok.value
3187                        ),
3188                        line: tok.line,
3189                        column: tok.column,
3190                        ..Default::default()
3191                    };
3192                    if num.ttype != TokenType::Integer {
3193                        return Err(bad(&num));
3194                    }
3195                    let value = num.value.parse::<u32>().map_err(|_| bad(&num))?;
3196                    self.advance();
3197                    node.requires_context = Some(value);
3198                }
3199                // §Fase 91.a — `now: "<IANA-tz>"`: the step's declared cognitive
3200                // timezone. A string literal; the format law (IANA shape) is the
3201                // type-checker's job (`axon-T892`) — the parser only requires a
3202                // string token here, surfaced at the exact column.
3203                TokenType::Identifier if inner.value == "now" => {
3204                    self.advance();
3205                    self.consume(TokenType::Colon)?;
3206                    let tz = self.current().clone();
3207                    if tz.ttype != TokenType::StringLit {
3208                        return Err(ParseError {
3209                            message: format!(
3210                                "`now:` must be an IANA timezone string literal like \
3211                                 \"America/Bogota\" or \"UTC\" (got '{}')",
3212                                tz.value
3213                            ),
3214                            line: tz.line,
3215                            column: tz.column,
3216                            ..Default::default()
3217                        });
3218                    }
3219                    self.advance();
3220                    node.now_tz = Some(tz.value);
3221                }
3222                // §Fase 54.a — a `use` nested inside a `step { }` body used
3223                // to be skipped structurally (grouped with the sub-constructs
3224                // below), silently degrading the tool dispatch to an
3225                // unconstrained LLM step with NO diagnostic. That fallthrough
3226                // drops the AST node before the type-checker can see it, so the
3227                // resource the tool would provision is never linearly accounted
3228                // for (use_tool soundness). Reject it here, at the parser —
3229                // the only place that still sees the token — and redirect to
3230                // the canonical forms.
3231                TokenType::Use => {
3232                    let tool = self
3233                        .tokens
3234                        .get(self.pos + 1)
3235                        .map(|t| t.value.as_str())
3236                        .filter(|v| !v.is_empty())
3237                        .unwrap_or("<Tool>");
3238                    return Err(ParseError {
3239                        message: format!(
3240                            "`use` is not valid inside a `step {{ }}` body — the tool dispatch \
3241                             would be silently dropped. To invoke a tool, either write the \
3242                             flow-level step `use {tool} on <arg>` (outside this block), or bind \
3243                             it inside this step with `apply: {tool}`. To attach a persona, put \
3244                             it in the step header: `step <name> use <Persona> {{ … }}`."
3245                        ),
3246                        line: inner.line,
3247                        column: inner.column,
3248                        ..Default::default()
3249                    });
3250                }
3251                // Sub-constructs (probe, reason, weave, stream) → skip structurally
3252                TokenType::Probe
3253                | TokenType::Reason
3254                | TokenType::Weave
3255                | TokenType::Stream => {
3256                    self.skip_flow_step_structural()?;
3257                }
3258                _ => {
3259                    return Err(ParseError {
3260                        message: format!(
3261                            "Unexpected token in step body: '{}' — expected given, ask, \
3262                             probe, reason, weave, stream, output, confidence_floor, navigate, \
3263                             apply, requires_context, now",
3264                            inner.value
3265                        ),
3266                        line: inner.line,
3267                        column: inner.column,
3268                                            ..Default::default()
3269                    });
3270                }
3271            }
3272        }
3273        self.consume(TokenType::RBrace)?;
3274        Ok(node)
3275    }
3276
3277    /// Skip a flow-level sub-construct structurally (consume keyword + args + optional block).
3278    fn skip_flow_step_structural(&mut self) -> Result<(), ParseError> {
3279        // Consume the keyword
3280        self.advance();
3281        // Consume tokens until we hit a { or a closing }, or a known flow step keyword
3282        while !self.check(TokenType::LBrace)
3283            && !self.check(TokenType::RBrace)
3284            && !self.check(TokenType::Eof)
3285        {
3286            // Check if we hit a new step-level keyword (means this was a one-liner)
3287            let tt = &self.current().ttype;
3288            if matches!(
3289                tt,
3290                TokenType::Step
3291                    | TokenType::Given
3292                    | TokenType::Ask
3293                    | TokenType::Output
3294                    | TokenType::Navigate
3295                    | TokenType::Use
3296                    | TokenType::Probe
3297                    | TokenType::Reason
3298                    | TokenType::Weave
3299                    | TokenType::Stream
3300                    | TokenType::If
3301                    | TokenType::For
3302                    | TokenType::Let
3303                    | TokenType::Return
3304            ) {
3305                return Ok(());
3306            }
3307            self.advance();
3308        }
3309        // If block, skip it
3310        if self.check(TokenType::LBrace) {
3311            self.skip_braced_block()?;
3312        }
3313        Ok(())
3314    }
3315
3316    // ── INTENT ───────────────────────────────────────────────────
3317
3318    fn parse_intent(&mut self) -> Result<IntentNode, ParseError> {
3319        let tok = self.consume(TokenType::Intent)?;
3320        let loc = self.loc_of(&tok);
3321        let name = self.consume(TokenType::Identifier)?.value;
3322        self.consume(TokenType::LBrace)?;
3323
3324        let mut node = IntentNode {
3325            name,
3326            given: String::new(),
3327            ask: String::new(),
3328            output_type: None,
3329            confidence_floor: None,
3330            loc,
3331            leading_trivia: Vec::new(),
3332            trailing_trivia: Vec::new(),
3333        };
3334
3335        while !self.check(TokenType::RBrace) {
3336            let field_name = self.current().value.clone();
3337            self.advance();
3338            self.consume(TokenType::Colon)?;
3339
3340            match field_name.as_str() {
3341                "given" => node.given = self.consume(TokenType::Identifier)?.value,
3342                "ask" => node.ask = self.consume(TokenType::StringLit)?.value,
3343                "output" => node.output_type = Some(self.parse_type_expr()?),
3344                "confidence_floor" => node.confidence_floor = Some(self.consume_number()?),
3345                _ => self.skip_value(),
3346            }
3347        }
3348        self.consume(TokenType::RBrace)?;
3349        Ok(node)
3350    }
3351
3352    // ── RUN ──────────────────────────────────────────────────────
3353
3354    fn parse_run(&mut self) -> Result<RunStatement, ParseError> {
3355        let tok = self.consume(TokenType::Run)?;
3356        let loc = self.loc_of(&tok);
3357        let flow_name = self.consume(TokenType::Identifier)?.value;
3358
3359        self.consume(TokenType::LParen)?;
3360        let mut arguments = Vec::new();
3361        if !self.check(TokenType::RParen) {
3362            arguments = self.parse_argument_list()?;
3363        }
3364        self.consume(TokenType::RParen)?;
3365
3366        let mut node = RunStatement {
3367            flow_name,
3368            arguments,
3369            persona: String::new(),
3370            context: String::new(),
3371            anchors: Vec::new(),
3372            on_failure: String::new(),
3373            on_failure_params: Vec::new(),
3374            output_to: String::new(),
3375            effort: String::new(),
3376            loc,
3377            leading_trivia: Vec::new(),
3378            trailing_trivia: Vec::new(),
3379        };
3380
3381        while self.check_run_modifier() {
3382            let mod_tok = self.current().clone();
3383            match mod_tok.ttype {
3384                TokenType::As => {
3385                    self.advance();
3386                    node.persona = self.consume(TokenType::Identifier)?.value;
3387                }
3388                TokenType::Within => {
3389                    self.advance();
3390                    node.context = self.consume(TokenType::Identifier)?.value;
3391                }
3392                TokenType::ConstrainedBy => {
3393                    self.advance();
3394                    node.anchors = self.parse_bracketed_identifiers()?;
3395                }
3396                TokenType::OnFailure => {
3397                    self.advance();
3398                    self.consume(TokenType::Colon)?;
3399                    node.on_failure = self.consume_any_ident_or_kw()?.value;
3400                    // Parse optional params: (key: val, ...)
3401                    if self.check(TokenType::LParen) {
3402                        self.advance();
3403                        while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
3404                            let key = self.consume_any_ident_or_kw()?.value;
3405                            self.consume(TokenType::Colon)?;
3406                            let val = self.consume_any_ident_or_kw()?.value;
3407                            node.on_failure_params.push((key, val));
3408                            if self.check(TokenType::Comma) {
3409                                self.advance();
3410                            }
3411                        }
3412                        if self.check(TokenType::RParen) {
3413                            self.advance();
3414                        }
3415                    }
3416                }
3417                TokenType::OutputTo => {
3418                    self.advance();
3419                    self.consume(TokenType::Colon)?;
3420                    node.output_to = self.consume(TokenType::StringLit)?.value;
3421                }
3422                TokenType::Effort => {
3423                    self.advance();
3424                    self.consume(TokenType::Colon)?;
3425                    node.effort = self.consume_any_ident_or_kw()?.value;
3426                }
3427                _ => break,
3428            }
3429        }
3430
3431        Ok(node)
3432    }
3433
3434    // ── EPISTEMIC BLOCK ──────────────────────────────────────────
3435
3436    fn parse_epistemic_block(&mut self) -> Result<EpistemicBlock, ParseError> {
3437        let tok = self.current().clone();
3438        let mode = match tok.ttype {
3439            TokenType::Know => "know",
3440            TokenType::Believe => "believe",
3441            TokenType::Speculate => "speculate",
3442            TokenType::Doubt => "doubt",
3443            _ => unreachable!(),
3444        };
3445        self.advance();
3446        let loc = self.loc_of(&tok);
3447
3448        self.consume(TokenType::LBrace)?;
3449        let mut body = Vec::new();
3450        while !self.check(TokenType::RBrace) {
3451            body.push(self.parse_declaration()?);
3452        }
3453        self.consume(TokenType::RBrace)?;
3454
3455        Ok(EpistemicBlock {
3456            mode: mode.to_string(),
3457            body,
3458            loc,
3459            leading_trivia: Vec::new(),
3460            trailing_trivia: Vec::new(),
3461        })
3462    }
3463
3464    // ── IF ────────────────────────────────────────────────────────
3465
3466    // ── §Fase 70.a — the pure expression engine (Pratt parser) ───────────
3467
3468    /// Parse a pure expression (§Fase 70). Precedence-climbing: `or` < `and` <
3469    /// comparison < `+ -` < `* / %` < unary (`- not`) < atom. Total + pure; no
3470    /// side effects. Field/index access + the builtin catalog land in §70.c/d.
3471    fn parse_expr(&mut self) -> Result<Expr, ParseError> {
3472        self.parse_expr_bp(0)
3473    }
3474
3475    fn parse_expr_bp(&mut self, min_bp: u8) -> Result<Expr, ParseError> {
3476        // Prefix: unary `-` (negation) / `not` (boolean). Binds tighter than
3477        // every binary operator (bp 6).
3478        let mut lhs = match self.current().ttype {
3479            TokenType::Minus => {
3480                self.advance();
3481                Expr::Unary(UnOp::Neg, Box::new(self.parse_expr_bp(6)?))
3482            }
3483            TokenType::Not => {
3484                self.advance();
3485                Expr::Unary(UnOp::Not, Box::new(self.parse_expr_bp(6)?))
3486            }
3487            _ => self.parse_postfix()?,
3488        };
3489        // Infix: left-associative (right_bp = left_bp + 1).
3490        while let Some((op, lbp)) = Self::binop_of(self.current().ttype.clone()) {
3491            if lbp < min_bp {
3492                break;
3493            }
3494            self.advance();
3495            let rhs = self.parse_expr_bp(lbp + 1)?;
3496            lhs = Expr::Binary(op, Box::new(lhs), Box::new(rhs));
3497        }
3498        Ok(lhs)
3499    }
3500
3501    /// Map a token to `(BinOp, left binding power)`, or `None` if it is not an
3502    /// infix operator (which stops the climb — e.g. at `->` or `{`).
3503    fn binop_of(t: TokenType) -> Option<(BinOp, u8)> {
3504        Some(match t {
3505            TokenType::Or => (BinOp::Or, 1),
3506            TokenType::And => (BinOp::And, 2),
3507            TokenType::Eq => (BinOp::Eq, 3),
3508            TokenType::Neq => (BinOp::Ne, 3),
3509            TokenType::Lt => (BinOp::Lt, 3),
3510            TokenType::Lte => (BinOp::Le, 3),
3511            TokenType::Gt => (BinOp::Gt, 3),
3512            TokenType::Gte => (BinOp::Ge, 3),
3513            TokenType::Plus => (BinOp::Add, 4),
3514            TokenType::Minus => (BinOp::Sub, 4),
3515            TokenType::Star => (BinOp::Mul, 5),
3516            TokenType::Slash => (BinOp::Div, 5),
3517            TokenType::Percent => (BinOp::Mod, 5),
3518            _ => return None,
3519        })
3520    }
3521
3522    /// §Fase 70.c — parse a primary then its `.` postfix chain: a builtin call
3523    /// (`.length`, `.contains(x)`) when the name is in the closed catalog, else
3524    /// a dotted reference-path continuation (`a.b.c` → `Ref("a.b.c")`, the
3525    /// pre-§70.c behaviour). Field access on a non-reference (`(a+b).x`) is
3526    /// reserved for §70.d.
3527    fn parse_postfix(&mut self) -> Result<Expr, ParseError> {
3528        let mut expr = self.parse_expr_atom()?;
3529        loop {
3530            if self.check(TokenType::Dot) {
3531                self.advance();
3532                let name = self.consume_any_ident_or_kw()?.value;
3533                if let Some(builtin) = Builtin::from_name(&name) {
3534                    let mut args = vec![expr];
3535                    if self.check(TokenType::LParen) {
3536                        self.advance();
3537                        while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
3538                            args.push(self.parse_expr_bp(0)?);
3539                            if self.check(TokenType::Comma) {
3540                                self.advance();
3541                            } else {
3542                                break;
3543                            }
3544                        }
3545                        self.consume(TokenType::RParen)?;
3546                    }
3547                    expr = Expr::Call(builtin, args);
3548                } else {
3549                    // §Fase 70.d — a plain dotted path on a Ref extends the Ref
3550                    // (back-compat: `a.b.c` → `Ref("a.b.c")`); on any other base
3551                    // it is a structured field access (the JSONB seam).
3552                    expr = match expr {
3553                        Expr::Ref(p) => Expr::Ref(format!("{p}.{name}")),
3554                        other => Expr::Field(Box::new(other), name),
3555                    };
3556                }
3557            } else if self.check(TokenType::LBracket) {
3558                // §Fase 70.d — index access `base[index]`.
3559                self.advance();
3560                let index = self.parse_expr_bp(0)?;
3561                self.consume(TokenType::RBracket)?;
3562                expr = Expr::Index(Box::new(expr), Box::new(index));
3563            } else {
3564                break;
3565            }
3566        }
3567        Ok(expr)
3568    }
3569
3570    fn parse_expr_atom(&mut self) -> Result<Expr, ParseError> {
3571        let tok = self.current().clone();
3572        match tok.ttype {
3573            TokenType::Integer => {
3574                self.advance();
3575                let lit = tok
3576                    .value
3577                    .parse::<i64>()
3578                    .map(ExprLit::Int)
3579                    .or_else(|_| tok.value.parse::<f64>().map(ExprLit::Float))
3580                    .map_err(|_| ParseError {
3581                        message: format!("invalid integer literal '{}'", tok.value),
3582                        line: tok.line,
3583                        column: tok.column,
3584                        ..Default::default()
3585                    })?;
3586                Ok(Expr::Lit(lit))
3587            }
3588            TokenType::Float => {
3589                self.advance();
3590                let f = tok.value.parse::<f64>().map_err(|_| ParseError {
3591                    message: format!("invalid float literal '{}'", tok.value),
3592                    line: tok.line,
3593                    column: tok.column,
3594                    ..Default::default()
3595                })?;
3596                Ok(Expr::Lit(ExprLit::Float(f)))
3597            }
3598            TokenType::Bool => {
3599                self.advance();
3600                Ok(Expr::Lit(ExprLit::Bool(tok.value == "true")))
3601            }
3602            TokenType::StringLit => {
3603                self.advance();
3604                Ok(Expr::Lit(ExprLit::Str(tok.value)))
3605            }
3606            TokenType::LParen => {
3607                self.advance();
3608                let inner = self.parse_expr_bp(0)?;
3609                self.consume(TokenType::RParen)?;
3610                Ok(inner)
3611            }
3612            _ => {
3613                // Reference: a single identifier (or keyword used as a name).
3614                // The `.` chain (dotted path / builtin call) is handled by the
3615                // postfix layer (§70.c `parse_postfix`).
3616                Ok(Expr::Ref(self.consume_any_ident_or_kw()?.value))
3617            }
3618        }
3619    }
3620
3621    /// §Fase 70.a — render a literal to its legacy surface string (for the
3622    /// back-compat `(condition, op, value)` triple). Only used when an
3623    /// expression fits the legacy shape; numeric round-tripping is exact for
3624    /// ints and faithful-enough for floats (the legacy runtime re-parses it).
3625    fn expr_lit_surface(lit: &ExprLit) -> String {
3626        match lit {
3627            ExprLit::Int(i) => i.to_string(),
3628            ExprLit::Float(f) => f.to_string(),
3629            ExprLit::Bool(b) => b.to_string(),
3630            ExprLit::Str(s) => s.clone(),
3631        }
3632    }
3633
3634    fn expr_leaf_surface(expr: &Expr) -> Option<String> {
3635        match expr {
3636            Expr::Ref(p) => Some(p.clone()),
3637            Expr::Lit(l) => Some(Self::expr_lit_surface(l)),
3638            _ => None,
3639        }
3640    }
3641
3642    /// A legacy "leaf" is a bare reference (truthy check) or a
3643    /// `<ref> <cmp> <ref|literal>` triple — exactly what the pre-§70 `if`
3644    /// grammar could express.
3645    fn expr_legacy_leaf(expr: &Expr) -> Option<(String, String, String)> {
3646        match expr {
3647            Expr::Ref(p) => Some((p.clone(), String::new(), String::new())),
3648            Expr::Binary(op, l, r) => {
3649                let op_s = match op {
3650                    BinOp::Eq => "==",
3651                    BinOp::Ne => "!=",
3652                    BinOp::Lt => "<",
3653                    BinOp::Le => "<=",
3654                    BinOp::Gt => ">",
3655                    BinOp::Ge => ">=",
3656                    _ => return None,
3657                };
3658                let lhs = match &**l {
3659                    Expr::Ref(p) => p.clone(),
3660                    _ => return None,
3661                };
3662                let rhs = Self::expr_leaf_surface(r)?;
3663                Some((lhs, op_s.to_string(), rhs))
3664            }
3665            _ => None,
3666        }
3667    }
3668
3669    /// Flatten an `or`-tree of legacy leaves in left-to-right order. Returns
3670    /// `false` (and leaves `out` unusable) if any node is not a legacy leaf.
3671    fn collect_or_leaves(expr: &Expr, out: &mut Vec<(String, String, String)>) -> bool {
3672        match expr {
3673            Expr::Binary(BinOp::Or, l, r) => {
3674                Self::collect_or_leaves(l, out) && Self::collect_or_leaves(r, out)
3675            }
3676            _ => match Self::expr_legacy_leaf(expr) {
3677                Some(t) => {
3678                    out.push(t);
3679                    true
3680                }
3681                None => false,
3682            },
3683        }
3684    }
3685
3686    /// §Fase 70.a — if the parsed condition fits the legacy
3687    /// `(condition, op, value)` + `or`-chain shape, return the legacy fields so
3688    /// the IR + runtime stay byte-identical to pre-§70 (zero drift). `None` ⇒
3689    /// the condition uses richer forms (`and`, `not`, arithmetic, parentheses,
3690    /// nesting) and must ride the `cond` expression evaluator.
3691    #[allow(clippy::type_complexity)]
3692    fn cond_as_legacy(
3693        expr: &Expr,
3694    ) -> Option<(String, String, String, Vec<(String, String, String)>, String)> {
3695        let mut leaves = Vec::new();
3696        if !Self::collect_or_leaves(expr, &mut leaves) || leaves.is_empty() {
3697            return None;
3698        }
3699        let (c0, o0, v0) = leaves[0].clone();
3700        let rest = leaves[1..].to_vec();
3701        let conjunctor = if rest.is_empty() {
3702            String::new()
3703        } else {
3704            "or".to_string()
3705        };
3706        Some((c0, o0, v0, rest, conjunctor))
3707    }
3708
3709    fn parse_if(&mut self) -> Result<ConditionalNode, ParseError> {
3710        let tok = self.consume(TokenType::If)?;
3711        let loc = self.loc_of(&tok);
3712
3713        // §Fase 70.a — parse the condition as a pure expression, then split:
3714        // a legacy-expressible condition populates the legacy triple fields
3715        // (cond = None → byte-identical IR + eval); a richer condition rides
3716        // the `cond` expression evaluator.
3717        let expr = self.parse_expr()?;
3718        let (condition, comparison_op, comparison_value, conditions, conjunctor, cond) =
3719            match Self::cond_as_legacy(&expr) {
3720                Some((c, o, v, more, conj)) => (c, o, v, more, conj, None),
3721                None => (
3722                    String::new(),
3723                    String::new(),
3724                    String::new(),
3725                    Vec::new(),
3726                    String::new(),
3727                    Some(expr),
3728                ),
3729            };
3730
3731        let mut then_body = Vec::new();
3732        let mut else_body = Vec::new();
3733
3734        // Arrow form or block form
3735        if self.check(TokenType::Arrow) {
3736            self.advance();
3737            then_body.push(self.parse_flow_step()?);
3738        } else if self.check(TokenType::LBrace) {
3739            self.advance();
3740            while !self.check(TokenType::RBrace) {
3741                then_body.push(self.parse_flow_step()?);
3742            }
3743            self.consume(TokenType::RBrace)?;
3744        }
3745
3746        // Else branch
3747        if self.check(TokenType::Else) {
3748            self.advance();
3749            if self.check(TokenType::Arrow) {
3750                self.advance();
3751                else_body.push(self.parse_flow_step()?);
3752            } else if self.check(TokenType::LBrace) {
3753                self.advance();
3754                while !self.check(TokenType::RBrace) {
3755                    else_body.push(self.parse_flow_step()?);
3756                }
3757                self.consume(TokenType::RBrace)?;
3758            }
3759        }
3760
3761        Ok(ConditionalNode {
3762            condition,
3763            comparison_op,
3764            comparison_value,
3765            then_body,
3766            else_body,
3767            conditions,
3768            conjunctor,
3769            cond,
3770            loc,
3771        })
3772    }
3773
3774    // ── FOR IN ───────────────────────────────────────────────────
3775
3776    fn parse_for_in(&mut self) -> Result<ForInStatement, ParseError> {
3777        let tok = self.consume(TokenType::For)?;
3778        let loc = self.loc_of(&tok);
3779        let variable = self.consume(TokenType::Identifier)?.value;
3780        self.consume(TokenType::In)?;
3781        let iterable = self.parse_dotted_identifier()?;
3782
3783        self.consume(TokenType::LBrace)?;
3784        // Fase 19.e — increment loop_depth so `parse_break` /
3785        // `parse_continue` inside the body pass the scope check.
3786        // Decrement on every exit path (Ok / Err) so a parse error
3787        // mid-body does not leave the depth permanently elevated
3788        // for later top-level parsing — `?` would skip the
3789        // decrement otherwise.
3790        self.loop_depth += 1;
3791        let body_result = (|| -> Result<Vec<FlowStep>, ParseError> {
3792            let mut body = Vec::new();
3793            while !self.check(TokenType::RBrace) {
3794                body.push(self.parse_flow_step()?);
3795            }
3796            Ok(body)
3797        })();
3798        self.loop_depth -= 1;
3799        let body = body_result?;
3800        self.consume(TokenType::RBrace)?;
3801
3802        Ok(ForInStatement {
3803            variable,
3804            iterable,
3805            body,
3806            loc,
3807        })
3808    }
3809
3810    /// Fase 19.e — `break` keyword. Compile-time scope check
3811    /// (`loop_depth == 0`) rejects break outside a for-in body.
3812    fn parse_break(&mut self) -> Result<BreakStatement, ParseError> {
3813        let tok = self.consume(TokenType::Break)?;
3814        let loc = self.loc_of(&tok);
3815        if self.loop_depth == 0 {
3816            return Err(ParseError {
3817                message: "'break' outside of a for-in loop body".to_string(),
3818                line: tok.line,
3819                column: tok.column,
3820                            ..Default::default()
3821            });
3822        }
3823        Ok(BreakStatement { loc })
3824    }
3825
3826    /// Fase 19.e — `continue` keyword. Same scope check as
3827    /// `parse_break`.
3828    fn parse_continue(&mut self) -> Result<ContinueStatement, ParseError> {
3829        let tok = self.consume(TokenType::Continue)?;
3830        let loc = self.loc_of(&tok);
3831        if self.loop_depth == 0 {
3832            return Err(ParseError {
3833                message: "'continue' outside of a for-in loop body".to_string(),
3834                line: tok.line,
3835                column: tok.column,
3836                            ..Default::default()
3837            });
3838        }
3839        Ok(ContinueStatement { loc })
3840    }
3841
3842    // ── LET ──────────────────────────────────────────────────────
3843
3844    fn parse_let(&mut self) -> Result<LetStatement, ParseError> {
3845        let tok = self.consume(TokenType::Let)?;
3846        let loc = self.loc_of(&tok);
3847
3848        // Name can be an identifier or a keyword used as binding name
3849        let name = self.consume_any_ident_or_kw()?.value;
3850        // §Fase 51.c.3 — optional type annotation `let x: <TypeExpr> = …`.
3851        let type_annotation = if self.check(TokenType::Colon) {
3852            self.advance();
3853            Some(self.parse_type_expr()?)
3854        } else {
3855            None
3856        };
3857        self.consume(TokenType::Assign)?;
3858        // Fase 17.a — reset side-channel before parsing value; the
3859        // atom / expr helpers tag the kind as they descend.
3860        self.last_let_value_kind = "literal".to_string();
3861        let (value, value_ast) = self.parse_let_value_expr_with_ast()?;
3862
3863        Ok(LetStatement {
3864            identifier: name,
3865            value_expr: value,
3866            value_kind: self.last_let_value_kind.clone(),
3867            type_annotation,
3868            value_ast,
3869            loc,
3870            leading_trivia: Vec::new(),
3871            trailing_trivia: Vec::new(),
3872        })
3873    }
3874
3875    fn parse_let_value_expr(&mut self) -> Result<String, ParseError> {
3876        let atom = self.parse_let_atom()?;
3877
3878        // Arithmetic expression: collect as string
3879        if matches!(
3880            self.current().ttype,
3881            TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
3882        ) {
3883            let mut parts = vec![atom];
3884            while matches!(
3885                self.current().ttype,
3886                TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
3887            ) {
3888                parts.push(self.advance().value.clone());
3889                parts.push(self.parse_let_atom()?);
3890            }
3891            self.last_let_value_kind = "expression".to_string();
3892            return Ok(parts.join(" "));
3893        }
3894        Ok(atom)
3895    }
3896
3897    /// §Fase 70.f — parse a `let`-binding value, additionally producing a
3898    /// structured `value_ast` for the expression case. A list literal keeps the
3899    /// dedicated path; everything else is parsed through the §70 expression
3900    /// engine and classified: a bare literal / reference keeps its pre-§70
3901    /// string form (`value_ast = None`, byte-identical), while a real expression
3902    /// (`price * qty`, `recent.length`) additionally carries a `value_ast` the
3903    /// runtime evaluates for real (pre-§70.f it was treated as an opaque literal
3904    /// string). Used ONLY by `parse_let` — other value positions (list items,
3905    /// remember/stream values) keep the string-only `parse_let_value_expr`.
3906    fn parse_let_value_expr_with_ast(&mut self) -> Result<(String, Option<Expr>), ParseError> {
3907        if self.check(TokenType::LBracket) {
3908            self.last_let_value_kind = "literal".to_string();
3909            return Ok((self.parse_let_list_literal()?, None));
3910        }
3911        let expr = self.parse_expr()?;
3912        Ok(match expr {
3913            Expr::Lit(lit) => {
3914                self.last_let_value_kind = "literal".to_string();
3915                (Self::expr_lit_surface(&lit), None)
3916            }
3917            Expr::Ref(p) => {
3918                self.last_let_value_kind = "reference".to_string();
3919                (p, None)
3920            }
3921            other => {
3922                self.last_let_value_kind = "expression".to_string();
3923                (Self::render_expr(&other), Some(other))
3924            }
3925        })
3926    }
3927
3928    /// §Fase 70.f — a readable surface rendering of an expression for the
3929    /// vestigial `value_expr` string (the runtime uses `value_ast`).
3930    fn render_expr(e: &Expr) -> String {
3931        match e {
3932            Expr::Lit(l) => Self::expr_lit_surface(l),
3933            Expr::Ref(p) => p.clone(),
3934            Expr::Unary(UnOp::Neg, x) => format!("-{}", Self::render_expr(x)),
3935            Expr::Unary(UnOp::Not, x) => format!("not {}", Self::render_expr(x)),
3936            Expr::Binary(op, l, r) => {
3937                let sym = match op {
3938                    BinOp::Add => "+",
3939                    BinOp::Sub => "-",
3940                    BinOp::Mul => "*",
3941                    BinOp::Div => "/",
3942                    BinOp::Mod => "%",
3943                    BinOp::Eq => "==",
3944                    BinOp::Ne => "!=",
3945                    BinOp::Lt => "<",
3946                    BinOp::Le => "<=",
3947                    BinOp::Gt => ">",
3948                    BinOp::Ge => ">=",
3949                    BinOp::And => "and",
3950                    BinOp::Or => "or",
3951                };
3952                format!("({} {sym} {})", Self::render_expr(l), Self::render_expr(r))
3953            }
3954            Expr::Call(b, args) => {
3955                let recv = args.first().map(Self::render_expr).unwrap_or_default();
3956                let rest: Vec<String> = args.iter().skip(1).map(Self::render_expr).collect();
3957                if rest.is_empty() {
3958                    format!("{recv}.{}", b.surface())
3959                } else {
3960                    format!("{recv}.{}({})", b.surface(), rest.join(", "))
3961                }
3962            }
3963            Expr::Field(b, f) => format!("{}.{f}", Self::render_expr(b)),
3964            Expr::Index(b, i) => format!("{}[{}]", Self::render_expr(b), Self::render_expr(i)),
3965        }
3966    }
3967
3968    fn parse_let_atom(&mut self) -> Result<String, ParseError> {
3969        let tok = self.current().clone();
3970
3971        match tok.ttype {
3972            TokenType::StringLit => {
3973                self.last_let_value_kind = "literal".to_string();
3974                self.advance();
3975                Ok(tok.value)
3976            }
3977            TokenType::Integer | TokenType::Float => {
3978                self.last_let_value_kind = "literal".to_string();
3979                self.advance();
3980                Ok(tok.value)
3981            }
3982            TokenType::Bool => {
3983                self.last_let_value_kind = "literal".to_string();
3984                self.advance();
3985                Ok(tok.value)
3986            }
3987            TokenType::Identifier => {
3988                self.last_let_value_kind = "reference".to_string();
3989                self.parse_dotted_identifier()
3990            }
3991            TokenType::LBracket => {
3992                self.last_let_value_kind = "literal".to_string();
3993                self.parse_let_list_literal()
3994            }
3995            _ => {
3996                // Keywords starting a dotted path (pix.document_tree)
3997                if self.pos + 1 < self.tokens.len()
3998                    && self.tokens[self.pos + 1].ttype == TokenType::Dot
3999                {
4000                    self.last_let_value_kind = "reference".to_string();
4001                    return self.parse_dotted_identifier();
4002                }
4003                Err(ParseError {
4004                    message: format!(
4005                        "Expected value expression, found {:?}('{}')",
4006                        tok.ttype, tok.value
4007                    ),
4008                    line: tok.line,
4009                    column: tok.column,
4010                                    ..Default::default()
4011                })
4012            }
4013        }
4014    }
4015
4016    fn parse_let_list_literal(&mut self) -> Result<String, ParseError> {
4017        self.consume(TokenType::LBracket)?;
4018        let mut items = Vec::new();
4019        if !self.check(TokenType::RBracket) {
4020            items.push(self.parse_let_value_expr()?);
4021            while self.check(TokenType::Comma) {
4022                self.advance();
4023                if self.check(TokenType::RBracket) {
4024                    break; // trailing comma
4025                }
4026                items.push(self.parse_let_value_expr()?);
4027            }
4028        }
4029        self.consume(TokenType::RBracket)?;
4030        Ok(format!("[{}]", items.join(", ")))
4031    }
4032
4033    // ── RETURN ───────────────────────────────────────────────────
4034
4035    fn parse_return(&mut self) -> Result<ReturnStatement, ParseError> {
4036        let tok = self.consume(TokenType::Return)?;
4037        let loc = self.loc_of(&tok);
4038        let value = self.parse_let_value_expr()?;
4039        Ok(ReturnStatement {
4040            value_expr: value,
4041            loc,
4042        })
4043    }
4044
4045    // ── TIER 2 FLOW STEP HELPERS ────────────────────────────────────
4046
4047    /// Parse: keyword target (consumes keyword + one identifier/keyword-as-value).
4048    fn parse_flow_step_simple(&mut self, _kw: &str) -> Result<(Loc, String), ParseError> {
4049        let tok = self.current().clone();
4050        self.advance(); // consume keyword
4051        let target = if self.at_declaration_start()
4052            || self.check(TokenType::RBrace)
4053            || self.check(TokenType::Eof)
4054        {
4055            String::new()
4056        } else {
4057            self.consume_any_ident_or_kw()?.value.clone()
4058        };
4059        // Skip optional braced block
4060        if self.check(TokenType::LBrace) {
4061            self.skip_braced_block()?;
4062        }
4063        Ok((
4064            Loc {
4065                line: tok.line,
4066                column: tok.column,
4067            },
4068            target,
4069        ))
4070    }
4071
4072    /// Parse: keyword { ... } — block-level step, skip body structurally.
4073    fn parse_block_step(&mut self, _kw: &str) -> Result<Loc, ParseError> {
4074        let tok = self.current().clone();
4075        self.advance();
4076        // Skip optional arguments before brace
4077        while !self.check(TokenType::LBrace)
4078            && !self.check(TokenType::RBrace)
4079            && !self.check(TokenType::Eof)
4080            && !self.at_declaration_start()
4081        {
4082            self.advance();
4083        }
4084        if self.check(TokenType::LBrace) {
4085            self.skip_braced_block()?;
4086        }
4087        Ok(Loc {
4088            line: tok.line,
4089            column: tok.column,
4090        })
4091    }
4092
4093    /// §Fase 86 — parse `forge <Name>(seed: "<text>") -> <Type> { mode:,
4094    /// novelty:, depth:, branches:, constraints: }`. Real field capture
4095    /// (replacing the pre-§86 discard-everything stub). Strict closed-catalog:
4096    /// an unknown field is a hard parse error; all cross-field laws (Boden mode
4097    /// catalog, novelty range, depth/branches ≥ 1, `constraints:` → `anchor`)
4098    /// are §86.c type-checker territory.
4099    fn parse_forge_step(&mut self) -> Result<ForgeBlock, ParseError> {
4100        let tok = self.consume(TokenType::Forge)?;
4101        let name = self.consume(TokenType::Identifier)?.value;
4102        let mut node = ForgeBlock {
4103            name,
4104            novelty: 0.5,
4105            depth: 1,
4106            branches: 1,
4107            loc: Loc { line: tok.line, column: tok.column },
4108            ..Default::default()
4109        };
4110        // `(seed: "...")`
4111        self.consume(TokenType::LParen)?;
4112        let arg = self.consume_any_ident_or_kw()?.value;
4113        self.consume(TokenType::Colon)?;
4114        if arg != "seed" {
4115            return Err(self.error(&format!(
4116                "forge '{}' expects `seed:` as its argument, found `{arg}`",
4117                node.name
4118            )));
4119        }
4120        node.seed = self.consume(TokenType::StringLit)?.value;
4121        self.consume(TokenType::RParen)?;
4122        // `-> <Type>`
4123        self.consume(TokenType::Arrow)?;
4124        node.output_type = self.consume_any_ident_or_kw()?.value;
4125        // `{ fields }`
4126        self.consume(TokenType::LBrace)?;
4127        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4128            let field = self.consume_any_ident_or_kw()?.value;
4129            self.consume(TokenType::Colon)?;
4130            match field.as_str() {
4131                "mode" => node.mode = self.consume_any_ident_or_kw()?.value,
4132                "novelty" => node.novelty = self.consume_number()?,
4133                "depth" => {
4134                    node.depth = self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0)
4135                }
4136                "branches" => {
4137                    node.branches =
4138                        self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0)
4139                }
4140                "constraints" => node.constraints_ref = self.consume_any_ident_or_kw()?.value,
4141                other => {
4142                    return Err(self.error(&format!("unknown forge field `{other}`")))
4143                }
4144            }
4145            if self.check(TokenType::Comma) {
4146                self.consume(TokenType::Comma)?;
4147            }
4148        }
4149        self.consume(TokenType::RBrace)?;
4150        Ok(node)
4151    }
4152
4153    /// §Fase 65 — Parse `par { stmt1  stmt2  … }` into CONCURRENT branches.
4154    /// Each top-level flow statement inside the block is one branch (a
4155    /// single-statement body); they execute concurrently at runtime
4156    /// (`flow_dispatcher::parallel::run_branches_concurrently`). Before §65 the
4157    /// `par` body was skipped (`parse_block_step`), so the branches were lost
4158    /// and the handler ran as a stub. Multi-statement branches (grouping
4159    /// several steps into one sequential branch) are a future grammar
4160    /// extension; today the natural `par { step A  step B }` fans A and B out.
4161    fn parse_par_block(&mut self) -> Result<ParBlock, ParseError> {
4162        let tok = self.current().clone();
4163        self.advance(); // consume `par`
4164        self.consume(TokenType::LBrace)?;
4165        let mut branches: Vec<Vec<FlowStep>> = Vec::new();
4166        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4167            branches.push(vec![self.parse_flow_step()?]);
4168        }
4169        self.consume(TokenType::RBrace)?;
4170        Ok(ParBlock {
4171            branches,
4172            loc: Loc {
4173                line: tok.line,
4174                column: tok.column,
4175            },
4176        })
4177    }
4178
4179    /// §Fase 51.a — Parse the `quant` cognitive block surface.
4180    ///
4181    /// Grammar (the attribute header is OPTIONAL):
4182    /// ```text
4183    /// quant { <flow steps> }
4184    /// quant(encoding: amplitude, observable: M, qubits: 10,
4185    ///       depth: 4, bandwidth: 0.5, reupload: 3, backend: quant_sim) { <flow steps> }
4186    /// ```
4187    /// The bare form (the paper's example) leaves every attribute defaulted
4188    /// (`encoding = amplitude`, `effect = quant_sim`). The body is parsed into
4189    /// real nested `FlowStep`s — like `par` branches — so §51.b's Continuous
4190    /// Type Invariant scans actual AST rather than skipped tokens.
4191    /// §Fase 88.a — parse `warden(<target>) within <Scope> { <body> }`. The
4192    /// `within <Scope>` clause is MANDATORY at the grammar level (fail-closed by
4193    /// construction: a scopeless warden cannot be written); §88.c checks the
4194    /// scope RESOLVES + the target is in its allowlist.
4195    fn parse_warden(&mut self) -> Result<WardenBlock, ParseError> {
4196        let tok = self.consume(TokenType::Warden)?;
4197        // `(<target>)` — the resource under analysis.
4198        self.consume(TokenType::LParen)?;
4199        let target = self.consume_any_ident_or_kw()?.value;
4200        self.consume(TokenType::RParen)?;
4201        // `within <Scope>` — MANDATORY. Omitting it is a hard parse error.
4202        self.consume(TokenType::Within)?;
4203        let scope_ref = self.consume(TokenType::Identifier)?.value;
4204        let mut block = WardenBlock {
4205            target,
4206            scope_ref,
4207            body: Vec::new(),
4208            loc: Loc {
4209                line: tok.line,
4210                column: tok.column,
4211            },
4212        };
4213        // Body: real nested flow steps (like `quant`/`par`).
4214        self.consume(TokenType::LBrace)?;
4215        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4216            block.body.push(self.parse_flow_step()?);
4217        }
4218        self.consume(TokenType::RBrace)?;
4219        Ok(block)
4220    }
4221
4222    /// §Fase 88.a — parse `scope <Name> { targets: [ … ], depth: <ident>,
4223    /// approver: [requires] "<cap>" }`. Flat key:value block (the `cache` shape).
4224    /// Catalog + non-empty validation is §88.c. Unknown fields are a hard error
4225    /// (D83.7): a scope governs an offensive-capable analysis.
4226    fn parse_scope(&mut self) -> Result<ScopeDefinition, ParseError> {
4227        let tok = self.consume(TokenType::Scope)?;
4228        let name = self.consume(TokenType::Identifier)?.value;
4229        let mut node = ScopeDefinition {
4230            name,
4231            loc: Loc {
4232                line: tok.line,
4233                column: tok.column,
4234            },
4235            ..Default::default()
4236        };
4237        self.consume(TokenType::LBrace)?;
4238        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4239            let key = self.consume_any_ident_or_kw()?.value;
4240            self.consume(TokenType::Colon)?;
4241            match key.as_str() {
4242                "targets" => {
4243                    self.consume(TokenType::LBracket)?;
4244                    while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
4245                        let t = if self.check(TokenType::StringLit) {
4246                            self.consume(TokenType::StringLit)?.value
4247                        } else {
4248                            self.consume_any_ident_or_kw()?.value
4249                        };
4250                        node.targets.push(t);
4251                        if self.check(TokenType::Comma) {
4252                            self.advance();
4253                        }
4254                    }
4255                    self.consume(TokenType::RBracket)?;
4256                }
4257                "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
4258                "approver" => {
4259                    // Optional `requires` sugar before the capability string.
4260                    if self.current().value == "requires" {
4261                        self.advance();
4262                    }
4263                    node.approver = self.consume(TokenType::StringLit)?.value;
4264                }
4265                other => {
4266                    return Err(self.error(&format!(
4267                        "unknown scope field `{other}` in scope `{}` — expected \
4268                         `targets` / `depth` / `approver`",
4269                        node.name
4270                    )))
4271                }
4272            }
4273            if self.check(TokenType::Comma) {
4274                self.consume(TokenType::Comma)?;
4275            }
4276        }
4277        self.consume(TokenType::RBrace)?;
4278        Ok(node)
4279    }
4280
4281    fn parse_quant(&mut self) -> Result<QuantBlock, ParseError> {
4282        let tok = self.current().clone();
4283        self.advance(); // consume `quant`
4284
4285        let mut block = QuantBlock {
4286            encoding: None,
4287            observable: None,
4288            qubits: None,
4289            depth: None,
4290            bandwidth: None,
4291            reupload: None,
4292            // D1/D9 default backend: the CPU simulator effect. `qpu_native` is
4293            // opt-in via `backend: qpu_native`.
4294            effect: "quant_sim".to_string(),
4295            body: Vec::new(),
4296            loc: Loc {
4297                line: tok.line,
4298                column: tok.column,
4299            },
4300        };
4301
4302        // ── Optional attribute header: `(key: value, …)` ──
4303        if self.check(TokenType::LParen) {
4304            self.advance();
4305            while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
4306                let key = self.consume_any_ident_or_kw()?.value;
4307                self.consume(TokenType::Colon)?;
4308                match key.as_str() {
4309                    "encoding" => {
4310                        block.encoding = Some(self.consume_any_ident_or_kw()?.value)
4311                    }
4312                    "observable" => {
4313                        block.observable = Some(self.parse_dotted_identifier()?)
4314                    }
4315                    "qubits" => block.qubits = Some(self.consume_number()? as i64),
4316                    "depth" => block.depth = Some(self.consume_number()? as i64),
4317                    "bandwidth" => block.bandwidth = Some(self.consume_number()?),
4318                    // §Fase 69.c — data re-uploading layers.
4319                    "reupload" => block.reupload = Some(self.consume_number()? as i64),
4320                    // `backend:` selects the algebraic-effect tag (D1/D9).
4321                    "backend" => block.effect = self.consume_any_ident_or_kw()?.value,
4322                    other => {
4323                        return Err(ParseError {
4324                            message: format!(
4325                                "Unknown `quant` attribute `{other}` — expected one of \
4326                                 encoding, observable, qubits, depth, bandwidth, reupload, backend"
4327                            ),
4328                            line: self.current().line,
4329                            column: self.current().column,
4330                            ..Default::default()
4331                        });
4332                    }
4333                }
4334                // Optional comma between attributes (order-free, trailing-comma ok).
4335                if self.check(TokenType::Comma) {
4336                    self.advance();
4337                }
4338            }
4339            self.consume(TokenType::RParen)?;
4340        }
4341
4342        // ── Body: real nested flow steps (like `par`) ──
4343        self.consume(TokenType::LBrace)?;
4344        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4345            block.body.push(self.parse_flow_step()?);
4346        }
4347        self.consume(TokenType::RBrace)?;
4348
4349        Ok(block)
4350    }
4351
4352    /// §Fase 51.d.2 — Parse the `yield <expr>` measurement point. Reuses the
4353    /// `let`-value expression grammar (reference / literal / arithmetic) so the
4354    /// yielded value's tokenization intent is preserved in `value_kind`.
4355    fn parse_yield(&mut self) -> Result<YieldStatement, ParseError> {
4356        let tok = self.consume(TokenType::Yield)?;
4357        let loc = self.loc_of(&tok);
4358        self.last_let_value_kind = "literal".to_string();
4359        let value_expr = self.parse_let_value_expr()?;
4360        Ok(YieldStatement {
4361            value_expr,
4362            value_kind: self.last_let_value_kind.clone(),
4363            loc,
4364        })
4365    }
4366
4367    /// Parse: keyword Name on target -> output_type (apply pattern).
4368    fn parse_apply_step(&mut self, _kw: &str) -> Result<(Loc, String, String, String), ParseError> {
4369        let tok = self.current().clone();
4370        self.advance(); // consume keyword
4371        let name = self.consume_any_ident_or_kw()?.value.clone();
4372        let mut target = String::new();
4373        let mut output_type = String::new();
4374        // "on" target
4375        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4376            let next = self.current().clone();
4377            if next.value == "on" {
4378                self.advance();
4379                target = self.consume_any_ident_or_kw()?.value.clone();
4380            }
4381        }
4382        // -> output_type
4383        if self.check(TokenType::Arrow) {
4384            self.advance();
4385            output_type = self.consume_any_ident_or_kw()?.value.clone();
4386        }
4387        // Skip optional braced block
4388        if self.check(TokenType::LBrace) {
4389            self.skip_braced_block()?;
4390        }
4391        Ok((
4392            Loc {
4393                line: tok.line,
4394                column: tok.column,
4395            },
4396            name,
4397            target,
4398            output_type,
4399        ))
4400    }
4401
4402    fn parse_weave_step(&mut self) -> Result<FlowStep, ParseError> {
4403        let tok = self.current().clone();
4404        self.advance();
4405        let mut node = WeaveStep {
4406            sources: Vec::new(),
4407            target: String::new(),
4408            format_type: String::new(),
4409            priority: Vec::new(),
4410            style: String::new(),
4411            loc: Loc {
4412                line: tok.line,
4413                column: tok.column,
4414            },
4415        };
4416        if self.check(TokenType::LBrace) {
4417            self.advance();
4418            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4419                let f = self.current().value.clone();
4420                self.advance();
4421                if self.check(TokenType::Colon) {
4422                    self.advance();
4423                    match f.as_str() {
4424                        "sources" => node.sources = self.parse_bracketed_identifiers()?,
4425                        "target" => node.target = self.consume_any_ident_or_kw()?.value.clone(),
4426                        "format" => {
4427                            node.format_type = self.consume_any_ident_or_kw()?.value.clone()
4428                        }
4429                        "priority" => node.priority = self.parse_bracketed_identifiers()?,
4430                        "style" => node.style = self.consume_any_ident_or_kw()?.value.clone(),
4431                        _ => self.skip_value(),
4432                    }
4433                }
4434            }
4435            if self.check(TokenType::RBrace) {
4436                self.advance();
4437            }
4438        }
4439        Ok(FlowStep::Weave(node))
4440    }
4441
4442    fn parse_use_step(&mut self) -> Result<FlowStep, ParseError> {
4443        let tok = self.current().clone();
4444        self.advance();
4445        let tool_name = self.consume_any_ident_or_kw()?.value.clone();
4446        // §Fase 58.b — two mutually-exclusive `use` argument surfaces:
4447        //   * `use Tool(query = "${q}", max_results = 5)` — D2 canonical
4448        //     multi-field keyword args (§58.b `UseArgs::Named`).
4449        //   * `use Tool on "${arg}"` / `on query` — the §54.b single positional
4450        //     argument (D5 back-compat, `UseArgs::LegacyPositional`):
4451        //       - a STRING LITERAL carrying interpolation (`on "${query}"`)
4452        //         resolved at dispatch against request-bound flow params;
4453        //       - a BARE identifier / literal (`on query` / `on 42`) verbatim.
4454        //     (Unquoted `${query}` is intentionally NOT a form — interpolation
4455        //     lives inside string literals everywhere in Axon.)
4456        let args = if self.check(TokenType::LParen) {
4457            UseArgs::Named(self.parse_named_arg_list()?)
4458        } else {
4459            let mut argument = String::new();
4460            if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4461                let next = self.current().clone();
4462                if next.value == "on" {
4463                    self.advance();
4464                    argument = self.consume_any_ident_or_kw()?.value.clone();
4465                }
4466            }
4467            UseArgs::LegacyPositional(argument)
4468        };
4469        if self.check(TokenType::LBrace) {
4470            self.skip_braced_block()?;
4471        }
4472        Ok(FlowStep::UseTool(UseToolStep {
4473            tool_name,
4474            args,
4475            loc: Loc {
4476                line: tok.line,
4477                column: tok.column,
4478            },
4479        }))
4480    }
4481
4482    /// §Fase 58.b — parse `(name = value, …)` keyword args for the canonical
4483    /// `use Tool(...)` multi-field dispatch. Values are captured as expression
4484    /// strings (StringLit / Integer / Float / Bool / dotted identifier / list)
4485    /// via the shared `parse_let_atom`, since the frontend has no structured
4486    /// `Expr`. A trailing comma is tolerated; `()` yields no args.
4487    fn parse_named_arg_list(&mut self) -> Result<Vec<(String, String, String)>, ParseError> {
4488        self.consume(TokenType::LParen)?;
4489        let mut args = Vec::new();
4490        while !self.check(TokenType::RParen) {
4491            // Accept a keyword-as-name (`filter`, `type`, `from`, …) — real
4492            // adopter schemas use such names; the following `=` disambiguates.
4493            let name = self.consume_any_ident_or_kw()?.value;
4494            self.consume(TokenType::Assign)?;
4495            let value = self.parse_let_atom()?;
4496            // §Fase 60 — `parse_let_atom` classified the value (`"literal"` vs
4497            // `"reference"`); carry it so the runtime resolves a bare
4498            // identifier / `Step.output` as a binding lookup, not a literal.
4499            let value_kind = self.last_let_value_kind.clone();
4500            args.push((name, value, value_kind));
4501            if self.check(TokenType::Comma) {
4502                self.advance();
4503            } else {
4504                break;
4505            }
4506        }
4507        self.consume(TokenType::RParen)?;
4508        Ok(args)
4509    }
4510
4511    fn parse_remember_step(&mut self) -> Result<FlowStep, ParseError> {
4512        let tok = self.current().clone();
4513        self.advance();
4514        let expr = self.consume_any_ident_or_kw()?.value.clone();
4515        let mut mem = String::new();
4516        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4517            let next = self.current().clone();
4518            if next.value == "in" || next.ttype == TokenType::In {
4519                self.advance();
4520                mem = self.consume_any_ident_or_kw()?.value.clone();
4521            }
4522        }
4523        Ok(FlowStep::Remember(RememberStep {
4524            expression: expr,
4525            memory_target: mem,
4526            loc: Loc {
4527                line: tok.line,
4528                column: tok.column,
4529            },
4530        }))
4531    }
4532
4533    fn parse_recall_step(&mut self) -> Result<FlowStep, ParseError> {
4534        let tok = self.current().clone();
4535        self.advance();
4536        let query = if self.check(TokenType::StringLit) {
4537            self.consume(TokenType::StringLit)?.value.clone()
4538        } else {
4539            self.consume_any_ident_or_kw()?.value.clone()
4540        };
4541        let mut mem = String::new();
4542        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4543            let next = self.current().clone();
4544            if next.value == "from" || next.ttype == TokenType::From {
4545                self.advance();
4546                mem = self.consume_any_ident_or_kw()?.value.clone();
4547            }
4548        }
4549        Ok(FlowStep::Recall(RecallStep {
4550            query,
4551            memory_source: mem,
4552            loc: Loc {
4553                line: tok.line,
4554                column: tok.column,
4555            },
4556        }))
4557    }
4558
4559    fn parse_hibernate_step(&mut self) -> Result<FlowStep, ParseError> {
4560        let tok = self.current().clone();
4561        self.advance();
4562        let mut event = String::new();
4563        let mut timeout = String::new();
4564        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4565            event = self.consume_any_ident_or_kw()?.value.clone();
4566        }
4567        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4568            let next = self.current().clone();
4569            if next.ttype == TokenType::Duration {
4570                self.advance();
4571                timeout = next.value.clone();
4572            }
4573        }
4574        Ok(FlowStep::Hibernate(HibernateStep {
4575            event_name: event,
4576            timeout,
4577            loc: Loc {
4578                line: tok.line,
4579                column: tok.column,
4580            },
4581        }))
4582    }
4583
4584    /// §Fase 108.d — `focus <Dataspace> { where: "<filter>", select: [cols], as: <name> }`
4585    /// — σ_φ ∘ π_v over a declared dataspace. The `where:` string is the
4586    /// §35 data-plane filter grammar (D108.9, shared with retrieve /
4587    /// navigate). Pre-108.d the optional body was silently discarded.
4588    /// §Fase 109.a — `grad <letName> wrt <x> [as <name>]` /
4589    /// `grad <letName> wrt [a, b] as <name>`. The differentiation itself
4590    /// happens at CHECK/IR time (T931/T932 + the symbolic differentiator);
4591    /// the parser only captures the surface.
4592    fn parse_grad_step(&mut self) -> Result<FlowStep, ParseError> {
4593        let tok = self.current().clone();
4594        self.advance();
4595        let target = self.consume_any_ident_or_kw()?.value.clone();
4596        let mut wrt: Vec<String> = Vec::new();
4597        let mut output = String::new();
4598        if !self.at_declaration_start() && self.current().value == "wrt" {
4599            self.advance();
4600            if self.check(TokenType::LBracket) {
4601                wrt = self.parse_bracketed_identifiers()?;
4602            } else {
4603                wrt.push(self.consume_any_ident_or_kw()?.value.clone());
4604            }
4605        }
4606        if !self.at_declaration_start() && self.current().value == "as" {
4607            self.advance();
4608            output = self.consume_any_ident_or_kw()?.value.clone();
4609        }
4610        Ok(FlowStep::Grad(GradStep {
4611            target,
4612            wrt,
4613            output,
4614            loc: Loc {
4615                line: tok.line,
4616                column: tok.column,
4617            },
4618        }))
4619    }
4620
4621    fn parse_focus_step(&mut self) -> Result<FlowStep, ParseError> {
4622        let tok = self.current().clone();
4623        self.advance();
4624        let expression = if self.at_declaration_start()
4625            || self.check(TokenType::RBrace)
4626            || self.check(TokenType::Eof)
4627        {
4628            String::new()
4629        } else {
4630            self.consume_any_ident_or_kw()?.value.clone()
4631        };
4632        let mut where_expr = String::new();
4633        let mut select: Vec<String> = Vec::new();
4634        let mut output = String::new();
4635        if self.check(TokenType::LBrace) {
4636            self.advance();
4637            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4638                if self.check(TokenType::Comma) {
4639                    self.advance();
4640                    continue;
4641                }
4642                let f = self.current().value.clone();
4643                self.advance();
4644                if self.check(TokenType::Colon) {
4645                    self.advance();
4646                    match f.as_str() {
4647                        "where" => {
4648                            where_expr = self.consume(TokenType::StringLit)?.value.clone()
4649                        }
4650                        "select" => select = self.parse_bracketed_identifiers()?,
4651                        "as" | "alias" => {
4652                            output = self.consume_any_ident_or_kw()?.value.clone()
4653                        }
4654                        _ => self.skip_value(),
4655                    }
4656                }
4657            }
4658            if self.check(TokenType::RBrace) {
4659                self.advance();
4660            }
4661        }
4662        Ok(FlowStep::Focus(FocusStep {
4663            expression,
4664            where_expr,
4665            select,
4666            output,
4667            loc: Loc {
4668                line: tok.line,
4669                column: tok.column,
4670            },
4671        }))
4672    }
4673
4674    fn parse_associate_step(&mut self) -> Result<FlowStep, ParseError> {
4675        let tok = self.current().clone();
4676        self.advance();
4677        let left = self.consume_any_ident_or_kw()?.value.clone();
4678        let mut right = String::new();
4679        let mut using = String::new();
4680        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4681            right = self.consume_any_ident_or_kw()?.value.clone();
4682        }
4683        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4684            let next = self.current().clone();
4685            if next.value == "using" {
4686                self.advance();
4687                using = self.consume_any_ident_or_kw()?.value.clone();
4688            }
4689        }
4690        let mut output = String::new();
4691        if self.check(TokenType::LBrace) {
4692            self.advance();
4693            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4694                let f = self.current().value.clone();
4695                self.advance();
4696                if self.check(TokenType::Colon) {
4697                    self.advance();
4698                    match f.as_str() {
4699                        "as" | "alias" => output = self.consume_any_ident_or_kw()?.value.clone(),
4700                        _ => self.skip_value(),
4701                    }
4702                }
4703            }
4704            if self.check(TokenType::RBrace) {
4705                self.advance();
4706            }
4707        }
4708        Ok(FlowStep::Associate(AssociateStep {
4709            left,
4710            right,
4711            using_field: using,
4712            output,
4713            loc: Loc {
4714                line: tok.line,
4715                column: tok.column,
4716            },
4717        }))
4718    }
4719
4720    fn parse_aggregate_step(&mut self) -> Result<FlowStep, ParseError> {
4721        let tok = self.current().clone();
4722        self.advance();
4723        let target = self.consume_any_ident_or_kw()?.value.clone();
4724        let mut group_by = Vec::new();
4725        let mut alias = String::new();
4726        let mut compute: Vec<String> = Vec::new();
4727        let mut where_expr = String::new();
4728        if self.check(TokenType::LBrace) {
4729            self.advance();
4730            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4731                let f = self.current().value.clone();
4732                self.advance();
4733                if self.check(TokenType::Colon) {
4734                    self.advance();
4735                    match f.as_str() {
4736                        "group_by" => group_by = self.parse_bracketed_identifiers()?,
4737                        "alias" | "as" => alias = self.consume_any_ident_or_kw()?.value.clone(),
4738                        // §Fase 108.d — the closed aggregate catalog, kept
4739                        // RAW (`count`, `sum(score)`, …); T930 validates.
4740                        "compute" => compute = self.parse_bracketed_aggregates()?,
4741                        // §Fase 108.d — the data-plane where (D108.9).
4742                        "where" => where_expr = self.consume(TokenType::StringLit)?.value.clone(),
4743                        _ => self.skip_value(),
4744                    }
4745                }
4746            }
4747            if self.check(TokenType::RBrace) {
4748                self.advance();
4749            }
4750        }
4751        Ok(FlowStep::Aggregate(AggregateStep {
4752            target,
4753            group_by,
4754            alias,
4755            compute,
4756            where_expr,
4757            loc: Loc {
4758                line: tok.line,
4759                column: tok.column,
4760            },
4761        }))
4762    }
4763
4764    fn parse_explore_step(&mut self) -> Result<FlowStep, ParseError> {
4765        let tok = self.current().clone();
4766        self.advance();
4767        let target = self.consume_any_ident_or_kw()?.value.clone();
4768        let mut limit = None;
4769        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4770            if self.current().ttype == TokenType::Integer {
4771                limit = self.current().value.parse::<i64>().ok();
4772                self.advance();
4773            }
4774        }
4775        let mut output = String::new();
4776        if self.check(TokenType::LBrace) {
4777            self.advance();
4778            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4779                let f = self.current().value.clone();
4780                self.advance();
4781                if self.check(TokenType::Colon) {
4782                    self.advance();
4783                    match f.as_str() {
4784                        "as" | "alias" => output = self.consume_any_ident_or_kw()?.value.clone(),
4785                        _ => self.skip_value(),
4786                    }
4787                }
4788            }
4789            if self.check(TokenType::RBrace) {
4790                self.advance();
4791            }
4792        }
4793        Ok(FlowStep::ExploreStep(ExploreStepNode {
4794            target,
4795            limit,
4796            output,
4797            loc: Loc {
4798                line: tok.line,
4799                column: tok.column,
4800            },
4801        }))
4802    }
4803
4804    /// §Fase 108.d — parse `[count, sum(score), avg(x)]`: bracketed
4805    /// aggregate entries, each `ident` or `ident(ident)`, kept raw.
4806    fn parse_bracketed_aggregates(&mut self) -> Result<Vec<String>, ParseError> {
4807        let mut out = Vec::new();
4808        self.consume(TokenType::LBracket)?;
4809        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
4810            let name = self.consume_any_ident_or_kw()?.value.clone();
4811            if self.check(TokenType::LParen) {
4812                self.advance();
4813                let col = self.consume_any_ident_or_kw()?.value.clone();
4814                self.consume(TokenType::RParen)?;
4815                out.push(format!("{name}({col})"));
4816            } else {
4817                out.push(name);
4818            }
4819            if self.check(TokenType::Comma) {
4820                self.advance();
4821            }
4822        }
4823        self.consume(TokenType::RBracket)?;
4824        Ok(out)
4825    }
4826
4827    /// §Fase 108.c — the governed ingest step:
4828    ///
4829    /// ```text
4830    /// ingest <sourceRef> into <Dataspace> {
4831    ///     format: csv | json
4832    ///     limits { max_bytes: N, max_rows: N }
4833    /// }
4834    /// ```
4835    ///
4836    /// Until 108.c the body was consumed by `skip_braced_block()`. Now it
4837    /// is a closed grammar: `format:` (raw here; required + validated by
4838    /// `axon-T929`) and an optional `limits { … }` block whose bounds are
4839    /// enforced on the raw byte stream BEFORE parsing (§100). An unknown
4840    /// body entry is a parse error.
4841    fn parse_ingest_step(&mut self) -> Result<FlowStep, ParseError> {
4842        let tok = self.current().clone();
4843        self.advance();
4844        let source = self.consume_any_ident_or_kw()?.value.clone();
4845        let mut target = String::new();
4846        let mut format = String::new();
4847        let mut max_bytes: Option<u64> = None;
4848        let mut max_rows: Option<u64> = None;
4849        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4850            let next = self.current().clone();
4851            if next.value == "into" || next.ttype == TokenType::Into {
4852                self.advance();
4853                target = self.consume_any_ident_or_kw()?.value.clone();
4854            }
4855        }
4856        if self.check(TokenType::LBrace) {
4857            self.consume(TokenType::LBrace)?;
4858            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4859                // Optional separators between body entries.
4860                if self.check(TokenType::Comma) {
4861                    self.advance();
4862                    continue;
4863                }
4864                let entry = self.current().clone();
4865                match entry.value.as_str() {
4866                    "format" => {
4867                        self.advance();
4868                        self.consume(TokenType::Colon)?;
4869                        format = self.consume_any_ident_or_kw()?.value.clone();
4870                    }
4871                    "limits" => {
4872                        self.advance();
4873                        self.consume(TokenType::LBrace)?;
4874                        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4875                            let bound = self.current().clone();
4876                            self.advance();
4877                            self.consume(TokenType::Colon)?;
4878                            let num_tok = self.consume(TokenType::Integer)?.clone();
4879                            let value = num_tok.value.parse::<u64>().map_err(|_| ParseError {
4880                                message: format!(
4881                                    "ingest `limits` bound `{}` must be a non-negative \
4882                                     integer byte/row count, got `{}`.",
4883                                    bound.value, num_tok.value
4884                                ),
4885                                line: num_tok.line,
4886                                column: num_tok.column,
4887                                ..Default::default()
4888                            })?;
4889                            match bound.value.as_str() {
4890                                "max_bytes" => max_bytes = Some(value),
4891                                "max_rows" => max_rows = Some(value),
4892                                other => {
4893                                    return Err(ParseError {
4894                                        message: format!(
4895                                            "Unknown ingest limit `{other}`. The closed \
4896                                             limits grammar is `max_bytes: <N>` and \
4897                                             `max_rows: <N>` — bounds enforced on the raw \
4898                                             stream BEFORE parsing (§100).",
4899                                        ),
4900                                        line: bound.line,
4901                                        column: bound.column,
4902                                        ..Default::default()
4903                                    });
4904                                }
4905                            }
4906                            if self.check(TokenType::Comma) {
4907                                self.advance();
4908                            }
4909                        }
4910                        self.consume(TokenType::RBrace)?;
4911                    }
4912                    other => {
4913                        return Err(ParseError {
4914                            message: format!(
4915                                "Unknown entry `{other}` in ingest body. The closed \
4916                                 grammar is `format: csv|json` and \
4917                                 `limits {{ max_bytes: <N>, max_rows: <N> }}`.",
4918                            ),
4919                            line: entry.line,
4920                            column: entry.column,
4921                            ..Default::default()
4922                        });
4923                    }
4924                }
4925            }
4926            self.consume(TokenType::RBrace)?;
4927        }
4928        Ok(FlowStep::Ingest(IngestStep {
4929            source,
4930            target,
4931            format,
4932            max_bytes,
4933            max_rows,
4934            loc: Loc {
4935                line: tok.line,
4936                column: tok.column,
4937            },
4938        }))
4939    }
4940
4941    fn parse_navigate_step(&mut self) -> Result<FlowStep, ParseError> {
4942        let tok = self.current().clone();
4943        self.advance();
4944        let pix_name = self.consume_any_ident_or_kw()?.value.clone();
4945        let mut node = NavigateStep {
4946            pix_name,
4947            corpus_name: String::new(),
4948            query_expr: String::new(),
4949            trail_enabled: false,
4950            output_name: String::new(),
4951            seed: String::new(),
4952            budget: None,
4953            where_expr: String::new(),
4954            loc: Loc {
4955                line: tok.line,
4956                column: tok.column,
4957            },
4958        };
4959        if self.check(TokenType::LBrace) {
4960            self.advance();
4961            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4962                let f = self.current().value.clone();
4963                self.advance();
4964                if self.check(TokenType::Colon) {
4965                    self.advance();
4966                    match f.as_str() {
4967                        "corpus" => {
4968                            node.corpus_name = self.consume_any_ident_or_kw()?.value.clone()
4969                        }
4970                        "query" => {
4971                            node.query_expr = self.consume(TokenType::StringLit)?.value.clone()
4972                        }
4973                        "trail" => {
4974                            node.trail_enabled = self.consume_any_ident_or_kw()?.value == "true"
4975                        }
4976                        "output" | "as" => {
4977                            node.output_name = self.consume_any_ident_or_kw()?.value.clone()
4978                        }
4979                        // §Fase 63.B — MDN corpus-graph navigation.
4980                        "from" => node.seed = self.consume_any_ident_or_kw()?.value.clone(),
4981                        "budget" => node.budget = self.parse_optional_int(),
4982                        // §Fase 66 (Q2) — column-scoped navigation: a raw filter
4983                        // expr (mirrors `retrieve … where`) pushed to the SELECT
4984                        // that sources the corpus `documents:`/`relations:` rows,
4985                        // so a `corpus from axonstore` is scoped to a sub-tenant
4986                        // COLUMN (`where: "tenant_id == '${tenant_id}'"`), not just
4987                        // the axon-tenant RLS scope. Resolved by the §37.d filter
4988                        // compiler at runtime (`${name}` → `$N` bind params).
4989                        "where" => {
4990                            node.where_expr = self.consume(TokenType::StringLit)?.value.clone()
4991                        }
4992                        _ => self.skip_value(),
4993                    }
4994                }
4995            }
4996            if self.check(TokenType::RBrace) {
4997                self.advance();
4998            }
4999        }
5000        Ok(FlowStep::Navigate(node))
5001    }
5002
5003    fn parse_drill_step(&mut self) -> Result<FlowStep, ParseError> {
5004        let tok = self.current().clone();
5005        self.advance();
5006        let pix_name = self.consume_any_ident_or_kw()?.value.clone();
5007        let mut node = DrillStep {
5008            pix_name,
5009            subtree_path: String::new(),
5010            query_expr: String::new(),
5011            output_name: String::new(),
5012            loc: Loc {
5013                line: tok.line,
5014                column: tok.column,
5015            },
5016        };
5017        if self.check(TokenType::LBrace) {
5018            self.advance();
5019            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5020                let f = self.current().value.clone();
5021                self.advance();
5022                if self.check(TokenType::Colon) {
5023                    self.advance();
5024                    match f.as_str() {
5025                        "subtree" | "path" => {
5026                            node.subtree_path = self.consume(TokenType::StringLit)?.value.clone()
5027                        }
5028                        "query" => {
5029                            node.query_expr = self.consume(TokenType::StringLit)?.value.clone()
5030                        }
5031                        "output" | "as" => {
5032                            node.output_name = self.consume_any_ident_or_kw()?.value.clone()
5033                        }
5034                        _ => self.skip_value(),
5035                    }
5036                }
5037            }
5038            if self.check(TokenType::RBrace) {
5039                self.advance();
5040            }
5041        }
5042        Ok(FlowStep::Drill(node))
5043    }
5044
5045    fn parse_corroborate_step(&mut self) -> Result<FlowStep, ParseError> {
5046        let tok = self.current().clone();
5047        self.advance();
5048        let nav_ref = self.consume_any_ident_or_kw()?.value.clone();
5049        let mut output = String::new();
5050        if self.check(TokenType::Arrow) {
5051            self.advance();
5052            output = self.consume_any_ident_or_kw()?.value.clone();
5053        }
5054        Ok(FlowStep::Corroborate(CorroborateStep {
5055            navigate_ref: nav_ref,
5056            output_name: output,
5057            loc: Loc {
5058                line: tok.line,
5059                column: tok.column,
5060            },
5061        }))
5062    }
5063
5064    fn parse_listen_step(&mut self) -> Result<FlowStep, ParseError> {
5065        let tok = self.current().clone();
5066        self.advance();
5067        // §λ-L-E Fase 13 D4 — dual-mode listen:
5068        //   • String topic (legacy, deprecated since Fase 13)
5069        //   • Identifier (canonical: declared ChannelDefinition)
5070        let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
5071            (self.consume(TokenType::StringLit)?.value.clone(), false)
5072        } else {
5073            (self.consume_any_ident_or_kw()?.value.clone(), true)
5074        };
5075        let mut alias = String::new();
5076        if !self.at_declaration_start()
5077            && !self.check(TokenType::RBrace)
5078            && !self.check(TokenType::LBrace)
5079        {
5080            let next = self.current().clone();
5081            if next.value == "as" || next.ttype == TokenType::As {
5082                self.advance();
5083                alias = self.consume_any_ident_or_kw()?.value.clone();
5084            }
5085        }
5086        // §Fase 52.a — parse the handler body into real flow-steps (was
5087        // `skip_braced_block`'d, leaving the listener inert). The body runs on
5088        // each event / scheduled tick.
5089        let body = self.parse_listener_body()?;
5090        Ok(FlowStep::Listen(ListenStep {
5091            channel,
5092            channel_is_ref,
5093            event_alias: alias,
5094            body,
5095            loc: Loc {
5096                line: tok.line,
5097                column: tok.column,
5098            },
5099        }))
5100    }
5101
5102    /// §Fase 52.a — parse a `listen … { <flow steps> }` handler body. The body
5103    /// is OPTIONAL (a bodyless `listen channel` returns an empty Vec); when
5104    /// present, each statement is a real [`FlowStep`] (the same grammar as a
5105    /// flow / `quant` / `par` body), executed per trigger by the §52.c runtime.
5106    fn parse_listener_body(&mut self) -> Result<Vec<FlowStep>, ParseError> {
5107        let mut body = Vec::new();
5108        if self.check(TokenType::LBrace) {
5109            self.advance(); // consume `{`
5110            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5111                body.push(self.parse_flow_step()?);
5112            }
5113            self.consume(TokenType::RBrace)?;
5114        }
5115        Ok(body)
5116    }
5117
5118    fn parse_retrieve_step(&mut self) -> Result<FlowStep, ParseError> {
5119        let tok = self.current().clone();
5120        self.advance();
5121        let store = self.consume_any_ident_or_kw()?.value.clone();
5122        let mut where_expr = String::new();
5123        let mut alias = String::new();
5124        let mut order_by = String::new();
5125        let mut limit_expr = String::new();
5126        let mut aggregate = String::new();
5127        let mut group_by = String::new();
5128        let mut cache = String::new();
5129        if self.check(TokenType::LBrace) {
5130            self.advance();
5131            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5132                let f = self.current().value.clone();
5133                self.advance();
5134                if self.check(TokenType::Colon) {
5135                    self.advance();
5136                    match f.as_str() {
5137                        "where" => where_expr = self.consume(TokenType::StringLit)?.value.clone(),
5138                        "as" | "alias" => alias = self.consume_any_ident_or_kw()?.value.clone(),
5139                        // §Fase 67.b — `order_by:` is a string literal
5140                        // (`"col asc, col2 desc"`), same surface as `where:`.
5141                        "order_by" => {
5142                            order_by = self.consume(TokenType::StringLit)?.value.clone()
5143                        }
5144                        // §Fase 67.b — `limit:` is a bare integer literal
5145                        // (`limit: 100`) OR a string carrying a binding
5146                        // (`limit: "${max}"`). Captured raw; the runtime
5147                        // resolves + validates it as a `u32`.
5148                        "limit" => {
5149                            let t = self.current().clone();
5150                            match t.ttype {
5151                                TokenType::Integer | TokenType::StringLit => {
5152                                    limit_expr = t.value.clone();
5153                                    self.advance();
5154                                }
5155                                _ => self.skip_value(),
5156                            }
5157                        }
5158                        // §Fase 76.d — `aggregate:` is a string literal from
5159                        // the CLOSED catalog (`"count"`, `"sum(tokens)"`, …);
5160                        // `group_by:` is a string literal listing columns
5161                        // (`"industry, status"`). Both captured raw; the
5162                        // §38.d proof (axon-T843/T844/T845) + the runtime
5163                        // (`filter::parse_aggregate_clause`) validate.
5164                        "aggregate" => {
5165                            aggregate = self.consume(TokenType::StringLit)?.value.clone()
5166                        }
5167                        "group_by" => {
5168                            group_by = self.consume(TokenType::StringLit)?.value.clone()
5169                        }
5170                        // §Fase 85.b — `cache:` names a declared `cache`
5171                        // policy. A retrieve reads a store (never `pure`), so
5172                        // caching it always accepts staleness — the checker
5173                        // requires a finite `ttl:` on the referenced cache
5174                        // (axon-T865) and resolves the reference (axon-T864).
5175                        "cache" => cache = self.consume_any_ident_or_kw()?.value.clone(),
5176                        _ => self.skip_value(),
5177                    }
5178                }
5179            }
5180            if self.check(TokenType::RBrace) {
5181                self.advance();
5182            }
5183        }
5184        Ok(FlowStep::Retrieve(RetrieveStep {
5185            store_name: store,
5186            where_expr,
5187            alias,
5188            order_by,
5189            limit_expr,
5190            aggregate,
5191            group_by,
5192            cache,
5193            loc: Loc {
5194                line: tok.line,
5195                column: tok.column,
5196            },
5197        }))
5198    }
5199
5200    /// §Fase 35.m — Parse a `purge` step, capturing the optional
5201    /// `{ where: "<expr>" }` filter. (Fase 35.p moved `mutate` to its
5202    /// own `parse_mutate_step`, which also captures SET columns; this
5203    /// helper now serves `purge` alone — a `DELETE` has no SET clause.)
5204    ///
5205    /// Before Fase 35.m these two steps parsed via `parse_flow_step_simple`,
5206    /// which *skipped* the braced block — so a written `where:` clause
5207    /// was silently dropped and every `mutate`/`purge` ran against the
5208    /// whole store, leaving the entire Fase 35.b/c parameterized-filter
5209    /// machinery unreachable for them. This mirror of `parse_retrieve_step`
5210    /// (minus the `as:` alias — a mutate/purge binds no result) closes
5211    /// that gap. Returns `(loc, store_name, where_expr)`.
5212    fn parse_store_where_step(
5213        &mut self,
5214    ) -> Result<(Loc, String, String), ParseError> {
5215        let tok = self.current().clone();
5216        self.advance(); // consume the keyword
5217        let store = if self.at_declaration_start()
5218            || self.check(TokenType::RBrace)
5219            || self.check(TokenType::Eof)
5220        {
5221            String::new()
5222        } else {
5223            self.consume_any_ident_or_kw()?.value.clone()
5224        };
5225        let mut where_expr = String::new();
5226        if self.check(TokenType::LBrace) {
5227            self.advance();
5228            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5229                let field = self.current().value.clone();
5230                self.advance();
5231                if self.check(TokenType::Colon) {
5232                    self.advance();
5233                    match field.as_str() {
5234                        "where" => {
5235                            where_expr =
5236                                self.consume(TokenType::StringLit)?.value.clone()
5237                        }
5238                        _ => self.skip_value(),
5239                    }
5240                }
5241            }
5242            if self.check(TokenType::RBrace) {
5243                self.advance();
5244            }
5245        }
5246        Ok((
5247            Loc {
5248                line: tok.line,
5249                column: tok.column,
5250            },
5251            store,
5252            where_expr,
5253        ))
5254    }
5255
5256    /// §Fase 35.o — Parse a `persist` step, capturing the optional
5257    /// `{ col: value }` field block.
5258    ///
5259    /// Before Fase 35.o `persist` parsed via `parse_flow_step_simple`,
5260    /// which *skipped* the braced block — so a written field block was
5261    /// silently dropped and the runtime fell back to writing every
5262    /// context binding as a row, which fails against any real table
5263    /// (flows always carry more bindings than a table has columns).
5264    /// This captures the declared columns into `PersistStep.fields`;
5265    /// the runtime writes exactly those (interpolated). A `persist`
5266    /// with no block keeps the v1.30.0 user-bindings fallback — fully
5267    /// backward-compatible. Mirror of `parse_retrieve_step`, but the
5268    /// keys are arbitrary column names rather than the fixed
5269    /// `where:` / `as:` filter keys.
5270    ///
5271    /// The optional `into` connector (`persist into <store>`) is
5272    /// accepted and skipped — before Fase 35.o `into` was captured as
5273    /// the store name.
5274    fn parse_persist_step(&mut self) -> Result<FlowStep, ParseError> {
5275        let tok = self.current().clone();
5276        self.advance(); // consume `persist`
5277        // Optional `into` connector — skip it so the store name that
5278        // follows is not mistaken for the target.
5279        if self.current().value == "into" && !self.check(TokenType::LBrace) {
5280            self.advance();
5281        }
5282        let store = if self.at_declaration_start()
5283            || self.check(TokenType::LBrace)
5284            || self.check(TokenType::RBrace)
5285            || self.check(TokenType::Eof)
5286        {
5287            String::new()
5288        } else {
5289            self.consume_any_ident_or_kw()?.value.clone()
5290        };
5291        let mut fields: Vec<(String, String)> = Vec::new();
5292        if self.check(TokenType::LBrace) {
5293            self.advance();
5294            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5295                let col = self.current().value.clone();
5296                self.advance();
5297                if self.check(TokenType::Colon) {
5298                    self.advance();
5299                    let value = if self.check(TokenType::StringLit) {
5300                        self.consume(TokenType::StringLit)?.value.clone()
5301                    } else if self.check(TokenType::RBrace)
5302                        || self.check(TokenType::Eof)
5303                        || self.check(TokenType::Colon)
5304                    {
5305                        String::new()
5306                    } else {
5307                        let v = self.current().clone();
5308                        self.advance();
5309                        v.value.clone()
5310                    };
5311                    fields.push((col, value));
5312                }
5313            }
5314            if self.check(TokenType::RBrace) {
5315                self.advance();
5316            }
5317        }
5318        Ok(FlowStep::Persist(PersistStep {
5319            store_name: store,
5320            fields,
5321            loc: Loc {
5322                line: tok.line,
5323                column: tok.column,
5324            },
5325        }))
5326    }
5327
5328    /// §Fase 35.p — Parse a `mutate` step, capturing both the
5329    /// `{ where: "<expr>" }` filter AND the `{ col: value }` SET
5330    /// assignments.
5331    ///
5332    /// Before Fase 35.p `mutate` parsed via `parse_store_where_step`,
5333    /// which captured only `where:` and *skipped* every other key — so
5334    /// the runtime built the `UPDATE … SET` clause from every flow
5335    /// binding (params + step results + `let`s), which fails against
5336    /// any real table (`column "X" does not exist`). This closes the
5337    /// gap symmetrically to 35.o's `persist` block: every key other
5338    /// than `where:` is a SET column; a `mutate` with no SET column
5339    /// keeps the v1.31.0 user-bindings fallback. `where:` keeps its
5340    /// string-literal grammar (as in `retrieve` / `purge`).
5341    fn parse_mutate_step(&mut self) -> Result<FlowStep, ParseError> {
5342        let tok = self.current().clone();
5343        self.advance(); // consume `mutate`
5344        let store = if self.at_declaration_start()
5345            || self.check(TokenType::LBrace)
5346            || self.check(TokenType::RBrace)
5347            || self.check(TokenType::Eof)
5348        {
5349            String::new()
5350        } else {
5351            self.consume_any_ident_or_kw()?.value.clone()
5352        };
5353        let mut where_expr = String::new();
5354        let mut fields: Vec<(String, String)> = Vec::new();
5355        if self.check(TokenType::LBrace) {
5356            self.advance();
5357            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5358                let key = self.current().value.clone();
5359                self.advance();
5360                if self.check(TokenType::Colon) {
5361                    self.advance();
5362                    if key == "where" {
5363                        where_expr =
5364                            self.consume(TokenType::StringLit)?.value.clone();
5365                    } else {
5366                        let value = if self.check(TokenType::StringLit) {
5367                            self.consume(TokenType::StringLit)?.value.clone()
5368                        } else if self.check(TokenType::RBrace)
5369                            || self.check(TokenType::Eof)
5370                            || self.check(TokenType::Colon)
5371                        {
5372                            String::new()
5373                        } else {
5374                            let v = self.current().clone();
5375                            self.advance();
5376                            v.value.clone()
5377                        };
5378                        fields.push((key, value));
5379                    }
5380                }
5381            }
5382            if self.check(TokenType::RBrace) {
5383                self.advance();
5384            }
5385        }
5386        Ok(FlowStep::Mutate(MutateStep {
5387            store_name: store,
5388            where_expr,
5389            fields,
5390            loc: Loc {
5391                line: tok.line,
5392                column: tok.column,
5393            },
5394        }))
5395    }
5396
5397    // ── TIER 2 DECLARATIONS ────────────────────────────────────────
5398
5399    fn parse_agent(&mut self) -> Result<AgentDefinition, ParseError> {
5400        let tok = self.consume(TokenType::Agent)?;
5401        let name = self.consume(TokenType::Identifier)?.value;
5402        let mut node = AgentDefinition {
5403            name,
5404            goal: String::new(),
5405            tools: Vec::new(),
5406            memory_ref: String::new(),
5407            strategy: String::new(),
5408            on_stuck: String::new(),
5409            shield_ref: String::new(),
5410            max_iterations: None,
5411            max_tokens: None,
5412            max_time: String::new(),
5413            max_cost: None,
5414            loc: Loc {
5415                line: tok.line,
5416                column: tok.column,
5417            },
5418            leading_trivia: Vec::new(),
5419            trailing_trivia: Vec::new(),
5420        };
5421        // Skip optional parameters/return type before brace
5422        while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
5423            self.advance();
5424        }
5425        self.consume(TokenType::LBrace)?;
5426        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5427            let field = self.current().clone();
5428            let field_name = field.value.clone();
5429            self.advance();
5430            if self.check(TokenType::Colon) {
5431                self.advance();
5432                match field_name.as_str() {
5433                    "goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
5434                    "tools" => node.tools = self.parse_bracketed_identifiers()?,
5435                    "memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
5436                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
5437                    "on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
5438                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
5439                    "max_iterations" => node.max_iterations = self.parse_optional_int(),
5440                    "max_tokens" => node.max_tokens = self.parse_optional_int(),
5441                    "max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
5442                    "max_cost" => node.max_cost = self.parse_optional_float(),
5443                    _ => self.skip_value(),
5444                }
5445            } else if self.check(TokenType::LBrace) {
5446                self.skip_braced_block()?;
5447            }
5448        }
5449        self.consume(TokenType::RBrace)?;
5450        Ok(node)
5451    }
5452
5453    /// §Fase 53 — `extension Name { category: effects|scan, members: [ … ] }`.
5454    /// The parser is permissive on field/category VALUES (validated in
5455    /// §53.c by the type-checker — no-shadowing, category-membership);
5456    /// it only enforces the structural grammar here.
5457    fn parse_extension(&mut self) -> Result<ExtensionDefinition, ParseError> {
5458        let tok = self.consume(TokenType::Extension)?;
5459        let name = self.consume(TokenType::Identifier)?.value;
5460        let mut node = ExtensionDefinition {
5461            name,
5462            category: String::new(),
5463            members: Vec::new(),
5464            loc: Loc {
5465                line: tok.line,
5466                column: tok.column,
5467            },
5468            leading_trivia: Vec::new(),
5469            trailing_trivia: Vec::new(),
5470        };
5471        self.consume(TokenType::LBrace)?;
5472        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5473            let field_name = self.current().value.clone();
5474            self.advance();
5475            if self.check(TokenType::Colon) {
5476                self.advance();
5477                match field_name.as_str() {
5478                    "category" => {
5479                        node.category = self.consume_any_ident_or_kw()?.value.clone()
5480                    }
5481                    "members" => node.members = self.parse_extension_members()?,
5482                    _ => self.skip_value(),
5483                }
5484            } else if self.check(TokenType::LBrace) {
5485                self.skip_braced_block()?;
5486            }
5487        }
5488        self.consume(TokenType::RBrace)?;
5489        Ok(node)
5490    }
5491
5492    /// §Fase 53 — parse `[ "name" [ : { semantics: "…", default_confidence: 0.8 } ], … ]`.
5493    /// Each member is a string literal optionally followed by a metadata
5494    /// block. Trailing/interleaved commas are tolerated.
5495    fn parse_extension_members(&mut self) -> Result<Vec<ExtensionMember>, ParseError> {
5496        let mut members = Vec::new();
5497        self.consume(TokenType::LBracket)?;
5498        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
5499            let name_tok = self.consume(TokenType::StringLit)?;
5500            let mut member = ExtensionMember {
5501                name: name_tok.value.clone(),
5502                semantics: None,
5503                default_confidence: None,
5504                loc: Loc {
5505                    line: name_tok.line,
5506                    column: name_tok.column,
5507                },
5508            };
5509            // Optional `: { semantics: "…", default_confidence: 0.8 }`.
5510            if self.check(TokenType::Colon) {
5511                self.advance();
5512                self.consume(TokenType::LBrace)?;
5513                while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5514                    let mkey = self.current().value.clone();
5515                    self.advance();
5516                    if self.check(TokenType::Colon) {
5517                        self.advance();
5518                        match mkey.as_str() {
5519                            "semantics" => {
5520                                member.semantics =
5521                                    Some(self.consume(TokenType::StringLit)?.value.clone())
5522                            }
5523                            "default_confidence" => {
5524                                member.default_confidence = self.parse_optional_float()
5525                            }
5526                            _ => self.skip_value(),
5527                        }
5528                    }
5529                    if self.check(TokenType::Comma) {
5530                        self.advance();
5531                    }
5532                }
5533                self.consume(TokenType::RBrace)?;
5534            }
5535            members.push(member);
5536            if self.check(TokenType::Comma) {
5537                self.advance();
5538            }
5539        }
5540        self.consume(TokenType::RBracket)?;
5541        Ok(members)
5542    }
5543
5544    /// §Fase 71.a/e — `window <Name> { timezone: "…"  allow: [ {days hours} ]
5545    /// exclude: [ "YYYY-MM-DD", … ]  on_outside: skip|defer|warn }`.
5546    fn parse_window(&mut self) -> Result<WindowDefinition, ParseError> {
5547        let tok = self.consume(TokenType::Window)?;
5548        let name = self.consume(TokenType::Identifier)?.value;
5549        let mut node = WindowDefinition {
5550            name,
5551            timezone: String::new(),
5552            allow: Vec::new(),
5553            exclude: Vec::new(),
5554            on_outside: String::new(),
5555            loc: Loc {
5556                line: tok.line,
5557                column: tok.column,
5558            },
5559            leading_trivia: Vec::new(),
5560            trailing_trivia: Vec::new(),
5561        };
5562        self.consume(TokenType::LBrace)?;
5563        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5564            let field_name = self.consume_any_ident_or_kw()?.value;
5565            self.consume(TokenType::Colon)?;
5566            match field_name.as_str() {
5567                "timezone" => node.timezone = self.consume(TokenType::StringLit)?.value,
5568                "allow" => node.allow = self.parse_window_allow()?,
5569                "exclude" => node.exclude = self.parse_window_exclude()?,
5570                "on_outside" => node.on_outside = self.consume_any_ident_or_kw()?.value,
5571                _ => self.skip_value(),
5572            }
5573        }
5574        self.consume(TokenType::RBrace)?;
5575        Ok(node)
5576    }
5577
5578    /// §Fase 71.a — the `allow: [ { … }, { … } ]` span list.
5579    fn parse_window_allow(&mut self) -> Result<Vec<WindowSpan>, ParseError> {
5580        self.consume(TokenType::LBracket)?;
5581        let mut spans = Vec::new();
5582        if !self.check(TokenType::RBracket) {
5583            spans.push(self.parse_window_span()?);
5584            while self.check(TokenType::Comma) {
5585                self.advance();
5586                if self.check(TokenType::RBracket) {
5587                    break; // trailing comma
5588                }
5589                spans.push(self.parse_window_span()?);
5590            }
5591        }
5592        self.consume(TokenType::RBracket)?;
5593        Ok(spans)
5594    }
5595
5596    /// §Fase 71.e — the `exclude: [ "YYYY-MM-DD", … ]` holiday list (ISO
5597    /// date-string literals; validated for real-calendar-date-ness by the
5598    /// `axon-T826` type check). An empty list / absent field ⇒ no holidays.
5599    fn parse_window_exclude(&mut self) -> Result<Vec<String>, ParseError> {
5600        self.consume(TokenType::LBracket)?;
5601        let mut dates = Vec::new();
5602        if !self.check(TokenType::RBracket) {
5603            dates.push(self.consume(TokenType::StringLit)?.value);
5604            while self.check(TokenType::Comma) {
5605                self.advance();
5606                if self.check(TokenType::RBracket) {
5607                    break; // trailing comma
5608                }
5609                dates.push(self.consume(TokenType::StringLit)?.value);
5610            }
5611        }
5612        self.consume(TokenType::RBracket)?;
5613        Ok(dates)
5614    }
5615
5616    /// §Fase 71.a — one span `{ days: Mon..Fri  hours: 9..18 }`.
5617    fn parse_window_span(&mut self) -> Result<WindowSpan, ParseError> {
5618        let tok = self.consume(TokenType::LBrace)?;
5619        let mut span = WindowSpan {
5620            day_start: String::new(),
5621            day_end: String::new(),
5622            hour_start: 0,
5623            hour_end: 0,
5624            loc: Loc {
5625                line: tok.line,
5626                column: tok.column,
5627            },
5628        };
5629        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5630            let field = self.consume_any_ident_or_kw()?.value;
5631            self.consume(TokenType::Colon)?;
5632            match field.as_str() {
5633                "days" => {
5634                    span.day_start = self.consume_any_ident_or_kw()?.value;
5635                    self.consume(TokenType::DotDot)?;
5636                    span.day_end = self.consume_any_ident_or_kw()?.value;
5637                }
5638                "hours" => {
5639                    span.hour_start = self.consume_number()? as i64;
5640                    self.consume(TokenType::DotDot)?;
5641                    span.hour_end = self.consume_number()? as i64;
5642                }
5643                _ => self.skip_value(),
5644            }
5645            if self.check(TokenType::Comma) {
5646                self.advance();
5647            }
5648        }
5649        self.consume(TokenType::RBrace)?;
5650        Ok(span)
5651    }
5652
5653    fn parse_shield(&mut self) -> Result<ShieldDefinition, ParseError> {
5654        let tok = self.consume(TokenType::Shield)?;
5655        let name = self.consume(TokenType::Identifier)?.value;
5656        let mut node = ShieldDefinition {
5657            name,
5658            scan: Vec::new(),
5659            strategy: String::new(),
5660            on_breach: String::new(),
5661            severity: String::new(),
5662            quarantine: String::new(),
5663            max_retries: None,
5664            confidence_threshold: None,
5665            allow_tools: Vec::new(),
5666            deny_tools: Vec::new(),
5667            sandbox: None,
5668            redact: Vec::new(),
5669            log: String::new(),
5670            deflect_message: String::new(),
5671            taint: String::new(),
5672            compliance: Vec::new(),
5673            sign: String::new(),
5674            unknown_fields: Vec::new(),
5675            loc: Loc {
5676                line: tok.line,
5677                column: tok.column,
5678            },
5679            leading_trivia: Vec::new(),
5680            trailing_trivia: Vec::new(),
5681        };
5682        self.consume(TokenType::LBrace)?;
5683        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5684            let field_name = self.current().value.clone();
5685            let field_loc = Loc {
5686                line: self.current().line,
5687                column: self.current().column,
5688            };
5689            self.advance();
5690            if self.check(TokenType::Colon) {
5691                self.advance();
5692                match field_name.as_str() {
5693                    "scan" => node.scan = self.parse_bracketed_identifiers()?,
5694                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
5695                    "on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
5696                    "severity" => node.severity = self.consume_any_ident_or_kw()?.value.clone(),
5697                    "quarantine" => {
5698                        node.quarantine = self.consume(TokenType::StringLit)?.value.clone()
5699                    }
5700                    "max_retries" => node.max_retries = self.parse_optional_int(),
5701                    "confidence_threshold" => {
5702                        node.confidence_threshold = self.parse_optional_float()
5703                    }
5704                    "allow_tools" => node.allow_tools = self.parse_bracketed_identifiers()?,
5705                    "deny_tools" => node.deny_tools = self.parse_bracketed_identifiers()?,
5706                    "sandbox" => {
5707                        node.sandbox = Some(self.consume_any_ident_or_kw()?.value == "true")
5708                    }
5709                    "redact" => node.redact = self.parse_bracketed_identifiers()?,
5710                    "log" => node.log = self.consume_any_ident_or_kw()?.value.clone(),
5711                    "deflect_message" => {
5712                        node.deflect_message = self.consume(TokenType::StringLit)?.value.clone()
5713                    }
5714                    "taint" => node.taint = self.consume_any_ident_or_kw()?.value.clone(),
5715                    // ESK Fase 6.1 — covered regulatory classes.
5716                    "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
5717                    // §Fase 77.a — egress signing algorithm (closed catalog,
5718                    // validated by the checker: `axon-T846`).
5719                    "sign" => node.sign = self.consume_any_ident_or_kw()?.value.clone(),
5720                    // §Fase 77.a — the value is still skipped (leniency
5721                    // preserved) but the NAME is recorded so the checker
5722                    // emits `axon-W010` instead of a silent drop.
5723                    _ => {
5724                        node.unknown_fields.push((field_name.clone(), field_loc));
5725                        self.skip_value()
5726                    }
5727                }
5728            } else if self.check(TokenType::LBrace) {
5729                self.skip_braced_block()?;
5730            }
5731        }
5732        self.consume(TokenType::RBrace)?;
5733        Ok(node)
5734    }
5735
5736    fn parse_pix(&mut self) -> Result<PixDefinition, ParseError> {
5737        let tok = self.consume(TokenType::Pix)?;
5738        let name = self.consume(TokenType::Identifier)?.value;
5739        let mut node = PixDefinition {
5740            name,
5741            source: String::new(),
5742            depth: None,
5743            branching: None,
5744            model: String::new(),
5745            loc: Loc {
5746                line: tok.line,
5747                column: tok.column,
5748            },
5749            leading_trivia: Vec::new(),
5750            trailing_trivia: Vec::new(),
5751        };
5752        self.consume(TokenType::LBrace)?;
5753        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5754            let field_name = self.current().value.clone();
5755            self.advance();
5756            if self.check(TokenType::Colon) {
5757                self.advance();
5758                match field_name.as_str() {
5759                    "source" => node.source = self.consume(TokenType::StringLit)?.value.clone(),
5760                    "depth" => node.depth = self.parse_optional_int(),
5761                    "branching" => node.branching = self.parse_optional_int(),
5762                    "model" => node.model = self.consume_any_ident_or_kw()?.value.clone(),
5763                    _ => self.skip_value(),
5764                }
5765            } else if self.check(TokenType::LBrace) {
5766                self.skip_braced_block()?;
5767            }
5768        }
5769        self.consume(TokenType::RBrace)?;
5770        Ok(node)
5771    }
5772
5773    /// §Fase 62.0 — `ledger <Name> { source, depth, branching, model }`.
5774    /// The append-only audit chain (formerly the Provenance-Index reading of
5775    /// `pix`). Field grammar mirrors `pix` (same shape) but the SEMANTICS are
5776    /// audit, not navigation: `depth` = chain retention, `branching` = Merkle
5777    /// factor, `model` = hash slug (sha256 / blake3 / sha3).
5778    fn parse_ledger(&mut self) -> Result<LedgerDefinition, ParseError> {
5779        let tok = self.consume(TokenType::Ledger)?;
5780        let name = self.consume(TokenType::Identifier)?.value;
5781        let mut node = LedgerDefinition {
5782            name,
5783            source: String::new(),
5784            depth: None,
5785            branching: None,
5786            model: String::new(),
5787            loc: Loc {
5788                line: tok.line,
5789                column: tok.column,
5790            },
5791            leading_trivia: Vec::new(),
5792            trailing_trivia: Vec::new(),
5793        };
5794        self.consume(TokenType::LBrace)?;
5795        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5796            let field_name = self.current().value.clone();
5797            self.advance();
5798            if self.check(TokenType::Colon) {
5799                self.advance();
5800                match field_name.as_str() {
5801                    "source" => node.source = self.consume(TokenType::StringLit)?.value.clone(),
5802                    "depth" => node.depth = self.parse_optional_int(),
5803                    "branching" => node.branching = self.parse_optional_int(),
5804                    "model" => node.model = self.consume_any_ident_or_kw()?.value.clone(),
5805                    _ => self.skip_value(),
5806                }
5807            } else if self.check(TokenType::LBrace) {
5808                self.skip_braced_block()?;
5809            }
5810        }
5811        self.consume(TokenType::RBrace)?;
5812        Ok(node)
5813    }
5814
5815    fn parse_psyche(&mut self) -> Result<PsycheDefinition, ParseError> {
5816        let tok = self.consume(TokenType::Psyche)?;
5817        let name = self.consume(TokenType::Identifier)?.value;
5818        let mut node = PsycheDefinition {
5819            name,
5820            dimensions: Vec::new(),
5821            manifold_noise: None,
5822            manifold_momentum: None,
5823            safety_constraints: Vec::new(),
5824            quantum_enabled: None,
5825            inference_mode: String::new(),
5826            loc: Loc {
5827                line: tok.line,
5828                column: tok.column,
5829            },
5830            leading_trivia: Vec::new(),
5831            trailing_trivia: Vec::new(),
5832        };
5833        self.consume(TokenType::LBrace)?;
5834        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5835            let field_name = self.current().value.clone();
5836            self.advance();
5837            if self.check(TokenType::Colon) {
5838                self.advance();
5839                match field_name.as_str() {
5840                    "dimensions" => node.dimensions = self.parse_bracketed_identifiers()?,
5841                    "manifold_noise" => node.manifold_noise = self.parse_optional_float(),
5842                    "manifold_momentum" => node.manifold_momentum = self.parse_optional_float(),
5843                    "safety_constraints" => {
5844                        node.safety_constraints = self.parse_bracketed_identifiers()?
5845                    }
5846                    "quantum_enabled" => {
5847                        node.quantum_enabled = Some(self.consume_any_ident_or_kw()?.value == "true")
5848                    }
5849                    "inference_mode" => {
5850                        node.inference_mode = self.consume_any_ident_or_kw()?.value.clone()
5851                    }
5852                    _ => self.skip_value(),
5853                }
5854            } else if self.check(TokenType::LBrace) {
5855                self.skip_braced_block()?;
5856            }
5857        }
5858        self.consume(TokenType::RBrace)?;
5859        Ok(node)
5860    }
5861
5862    fn parse_corpus(&mut self) -> Result<CorpusDefinition, ParseError> {
5863        let tok = self.consume(TokenType::Corpus)?;
5864        let name = self.consume(TokenType::Identifier)?.value;
5865        let mut node = CorpusDefinition {
5866            name,
5867            documents: Vec::new(),
5868            relations: Vec::new(),
5869            adaptive: false,
5870            mcp_server: String::new(),
5871            mcp_resource_uri: String::new(),
5872            store_source: None,
5873            loc: Loc {
5874                line: tok.line,
5875                column: tok.column,
5876            },
5877            leading_trivia: Vec::new(),
5878            trailing_trivia: Vec::new(),
5879        };
5880        // corpus Name from mcp("server", "uri")  — static MCP-bound short form.
5881        // corpus Name from axonstore { documents: S(id,title)  relations: … }  —
5882        // §Fase 64.A dynamic store-sourced MDN graph (falls through to the body).
5883        let mut dynamic = false;
5884        if self.check(TokenType::From) {
5885            self.advance();
5886            if self.check(TokenType::AxonStore) {
5887                self.advance();
5888                dynamic = true;
5889            } else {
5890                self.consume(TokenType::Mcp)?;
5891                self.consume(TokenType::LParen)?;
5892                node.mcp_server = self.consume(TokenType::StringLit)?.value.clone();
5893                self.consume(TokenType::Comma)?;
5894                node.mcp_resource_uri = self.consume(TokenType::StringLit)?.value.clone();
5895                self.consume(TokenType::RParen)?;
5896                return Ok(node);
5897            }
5898        }
5899        self.consume(TokenType::LBrace)?;
5900        // §Fase 64.A — accumulate the store-mapping pieces while the dynamic body
5901        // is parsed; folded into `node.store_source` after the closing brace.
5902        let mut src = CorpusStoreSource {
5903            doc_store: String::new(),
5904            doc_id_col: String::new(),
5905            doc_title_col: String::new(),
5906            edge_store: String::new(),
5907            edge_from_col: String::new(),
5908            edge_to_col: String::new(),
5909            edge_type_col: String::new(),
5910            edge_weight_col: String::new(),
5911            loc: Loc {
5912                line: tok.line,
5913                column: tok.column,
5914            },
5915        };
5916        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5917            let field_name = self.current().value.clone();
5918            self.advance();
5919            if self.check(TokenType::Colon) {
5920                self.advance();
5921                match field_name.as_str() {
5922                    // §Fase 64.A — dynamic: `documents: <DocStore>(id_col, title_col)`.
5923                    "documents" if dynamic => {
5924                        let (store, cols) = self.parse_corpus_store_mapping(2)?;
5925                        src.doc_store = store;
5926                        src.doc_id_col = cols[0].clone();
5927                        src.doc_title_col = cols[1].clone();
5928                    }
5929                    "documents" => node.documents = self.parse_bracketed_identifiers()?,
5930                    // §Fase 64.A — dynamic: `relations: <EdgeStore>(from, to, etype, weight)`.
5931                    "relations" if dynamic => {
5932                        let (store, cols) = self.parse_corpus_store_mapping(4)?;
5933                        src.edge_store = store;
5934                        src.edge_from_col = cols[0].clone();
5935                        src.edge_to_col = cols[1].clone();
5936                        src.edge_type_col = cols[2].clone();
5937                        src.edge_weight_col = cols[3].clone();
5938                    }
5939                    // §Fase 63.A — static typed weighted edges → MDN corpus graph.
5940                    "relations" => node.relations = self.parse_corpus_relations()?,
5941                    // §Fase 63.C — enable the memory endofunctor.
5942                    "adaptive" => node.adaptive = self.consume_any_ident_or_kw()?.value == "true",
5943                    _ => self.skip_value(),
5944                }
5945            } else if self.check(TokenType::LBrace) {
5946                self.skip_braced_block()?;
5947            }
5948        }
5949        self.consume(TokenType::RBrace)?;
5950        if dynamic {
5951            node.store_source = Some(src);
5952        }
5953        Ok(node)
5954    }
5955
5956    /// §Fase 64.A — parse a store-mapping `<StoreName>( col1, col2, … )` of exactly
5957    /// `n` columns. Used by the dynamic store-sourced corpus's `documents:` (2
5958    /// cols: id, title) and `relations:` (4 cols: from, to, etype, weight). The
5959    /// store name is an identifier (a declared `axonstore`); the columns may be
5960    /// keywords (a column could be named `from`/`type`), so they use the
5961    /// keyword-tolerant consumer. The type-checker validates store + columns.
5962    fn parse_corpus_store_mapping(&mut self, n: usize) -> Result<(String, Vec<String>), ParseError> {
5963        let store = self.consume(TokenType::Identifier)?.value.clone();
5964        self.consume(TokenType::LParen)?;
5965        let mut cols = Vec::with_capacity(n);
5966        for i in 0..n {
5967            if i > 0 {
5968                self.consume(TokenType::Comma)?;
5969            }
5970            cols.push(self.consume_any_ident_or_kw()?.value.clone());
5971        }
5972        self.consume(TokenType::RParen)?;
5973        Ok((store, cols))
5974    }
5975
5976    /// §Fase 63.A — parse `relations: [ etype(from, to, weight) … ]`, the typed
5977    /// weighted edges of an MDN corpus graph. Entries are whitespace/newline
5978    /// separated; commas between them are optional. Edge-type validity (closed
5979    /// catalog), document references, and the weight range are checked by the
5980    /// type-checker (`check_corpus`), not here.
5981    fn parse_corpus_relations(&mut self) -> Result<Vec<CorpusRelation>, ParseError> {
5982        let mut out = Vec::new();
5983        self.consume(TokenType::LBracket)?;
5984        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
5985            if self.check(TokenType::Comma) {
5986                self.advance();
5987                continue;
5988            }
5989            let tok = self.current().clone();
5990            let etype = self.consume_any_ident_or_kw()?.value.clone();
5991            self.consume(TokenType::LParen)?;
5992            let from = self.consume_any_ident_or_kw()?.value.clone();
5993            self.consume(TokenType::Comma)?;
5994            let to = self.consume_any_ident_or_kw()?.value.clone();
5995            self.consume(TokenType::Comma)?;
5996            let weight = self.consume_number()?;
5997            self.consume(TokenType::RParen)?;
5998            out.push(CorpusRelation {
5999                etype,
6000                from,
6001                to,
6002                weight,
6003                loc: Loc { line: tok.line, column: tok.column },
6004            });
6005        }
6006        self.consume(TokenType::RBracket)?;
6007        Ok(out)
6008    }
6009
6010    /// §Fase 108.b — the typed dataspace declaration:
6011    ///
6012    /// ```text
6013    /// dataspace <Name> {
6014    ///     column <name>: <Type>
6015    ///     …
6016    /// }
6017    /// ```
6018    ///
6019    /// Until 108.b the body was consumed by `skip_braced_block()` — any
6020    /// content, including garbage, compiled clean and reached nothing.
6021    /// Now each entry must be a `column` field; the declared type is
6022    /// kept RAW here and resolved against the closed 6-type catalog by
6023    /// the type-checker (`axon-T928`), so all schema errors accumulate
6024    /// in a single compile. An unknown body keyword is a parse error
6025    /// (the grammar is closed — the §38 axonstore posture).
6026    fn parse_dataspace(&mut self) -> Result<DataspaceDefinition, ParseError> {
6027        let tok = self.consume(TokenType::Dataspace)?;
6028        let name = self.consume(TokenType::Identifier)?.value;
6029        let mut node = DataspaceDefinition {
6030            name,
6031            columns: Vec::new(),
6032            loc: Loc {
6033                line: tok.line,
6034                column: tok.column,
6035            },
6036            leading_trivia: Vec::new(),
6037            trailing_trivia: Vec::new(),
6038        };
6039        if self.check(TokenType::LBrace) {
6040            self.consume(TokenType::LBrace)?;
6041            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6042                let entry = self.current().clone();
6043                if entry.value != "column" {
6044                    return Err(ParseError {
6045                        message: format!(
6046                            "Unknown entry `{}` in dataspace `{}`. A dataspace body \
6047                             declares its columnar schema: `column <name>: <Type>` \
6048                             (one per line, over the closed type catalog — \
6049                             Text, Int, Float, Bool, Timestamp, Json).",
6050                            entry.value, node.name
6051                        ),
6052                        line: entry.line,
6053                        column: entry.column,
6054                        ..Default::default()
6055                    });
6056                }
6057                self.advance(); // `column`
6058                let col_tok = self.current().clone();
6059                let col_name = self.consume_any_ident_or_kw()?.value.clone();
6060                self.consume(TokenType::Colon)?;
6061                let declared_type = self.consume_any_ident_or_kw()?.value.clone();
6062                node.columns.push(crate::ast::DataspaceColumn {
6063                    name: col_name,
6064                    declared_type,
6065                    loc: Loc {
6066                        line: col_tok.line,
6067                        column: col_tok.column,
6068                    },
6069                });
6070            }
6071            self.consume(TokenType::RBrace)?;
6072        }
6073        Ok(node)
6074    }
6075
6076    fn parse_ots(&mut self) -> Result<OtsDefinition, ParseError> {
6077        let tok = self.consume(TokenType::Ots)?;
6078        let name = self.consume(TokenType::Identifier)?.value;
6079        let mut node = OtsDefinition {
6080            name,
6081            teleology: String::new(),
6082            homotopy_search: String::new(),
6083            loss_function: String::new(),
6084            loc: Loc {
6085                line: tok.line,
6086                column: tok.column,
6087            },
6088            leading_trivia: Vec::new(),
6089            trailing_trivia: Vec::new(),
6090        };
6091        // Skip optional type params <In, Out>
6092        if self.check(TokenType::Lt) {
6093            while !self.check(TokenType::Gt) && !self.check(TokenType::Eof) {
6094                self.advance();
6095            }
6096            if self.check(TokenType::Gt) {
6097                self.advance();
6098            }
6099        }
6100        self.consume(TokenType::LBrace)?;
6101        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6102            let field_name = self.current().value.clone();
6103            self.advance();
6104            if self.check(TokenType::Colon) {
6105                self.advance();
6106                match field_name.as_str() {
6107                    "teleology" => {
6108                        node.teleology = self.consume(TokenType::StringLit)?.value.clone()
6109                    }
6110                    "homotopy_search" => {
6111                        node.homotopy_search = self.consume_any_ident_or_kw()?.value.clone()
6112                    }
6113                    "loss_function" => {
6114                        node.loss_function = self.consume(TokenType::StringLit)?.value.clone()
6115                    }
6116                    _ => self.skip_value(),
6117                }
6118            } else if self.check(TokenType::LBrace) {
6119                self.skip_braced_block()?;
6120            }
6121        }
6122        self.consume(TokenType::RBrace)?;
6123        Ok(node)
6124    }
6125
6126    fn parse_mandate(&mut self) -> Result<MandateDefinition, ParseError> {
6127        let tok = self.consume(TokenType::Mandate)?;
6128        let name = self.consume(TokenType::Identifier)?.value;
6129        let mut node = MandateDefinition {
6130            name,
6131            constraint: String::new(),
6132            kp: None,
6133            ki: None,
6134            kd: None,
6135            tolerance: None,
6136            max_steps: None,
6137            on_violation: String::new(),
6138            loc: Loc {
6139                line: tok.line,
6140                column: tok.column,
6141            },
6142            leading_trivia: Vec::new(),
6143            trailing_trivia: Vec::new(),
6144        };
6145        self.consume(TokenType::LBrace)?;
6146        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6147            let field_name = self.current().value.clone();
6148            self.advance();
6149            if self.check(TokenType::Colon) {
6150                self.advance();
6151                match field_name.as_str() {
6152                    "constraint" => {
6153                        node.constraint = self.consume(TokenType::StringLit)?.value.clone()
6154                    }
6155                    "kp" | "Kp" => node.kp = self.parse_optional_float(),
6156                    "ki" | "Ki" => node.ki = self.parse_optional_float(),
6157                    "kd" | "Kd" => node.kd = self.parse_optional_float(),
6158                    "tolerance" => node.tolerance = self.parse_optional_float(),
6159                    "max_steps" => node.max_steps = self.parse_optional_int(),
6160                    "on_violation" => {
6161                        node.on_violation = self.consume_any_ident_or_kw()?.value.clone()
6162                    }
6163                    _ => self.skip_value(),
6164                }
6165            } else if self.check(TokenType::LBrace) {
6166                self.skip_braced_block()?;
6167            }
6168        }
6169        self.consume(TokenType::RBrace)?;
6170        Ok(node)
6171    }
6172
6173    fn parse_compute(&mut self) -> Result<ComputeDefinition, ParseError> {
6174        let tok = self.consume(TokenType::Compute)?;
6175        let name = self.consume(TokenType::Identifier)?.value;
6176        let mut node = ComputeDefinition {
6177            name,
6178            shield_ref: String::new(),
6179            loc: Loc {
6180                line: tok.line,
6181                column: tok.column,
6182            },
6183            leading_trivia: Vec::new(),
6184            trailing_trivia: Vec::new(),
6185        };
6186        // Skip optional parameters/return type before brace
6187        while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
6188            self.advance();
6189        }
6190        self.consume(TokenType::LBrace)?;
6191        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6192            let field_name = self.current().value.clone();
6193            self.advance();
6194            if self.check(TokenType::Colon) {
6195                self.advance();
6196                match field_name.as_str() {
6197                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
6198                    _ => self.skip_value(),
6199                }
6200            } else if self.check(TokenType::LBrace) {
6201                self.skip_braced_block()?;
6202            }
6203        }
6204        self.consume(TokenType::RBrace)?;
6205        Ok(node)
6206    }
6207
6208    fn parse_daemon(&mut self) -> Result<DaemonDefinition, ParseError> {
6209        let tok = self.consume(TokenType::Daemon)?;
6210        let name = self.consume(TokenType::Identifier)?.value;
6211        let mut node = DaemonDefinition {
6212            name,
6213            goal: String::new(),
6214            tools: Vec::new(),
6215            memory_ref: String::new(),
6216            strategy: String::new(),
6217            on_stuck: String::new(),
6218            shield_ref: String::new(),
6219            window_ref: String::new(),
6220            budget: None,
6221            max_tokens: None,
6222            max_time: String::new(),
6223            max_cost: None,
6224            listeners: Vec::new(),
6225            requires_capabilities: Vec::new(),
6226            loc: Loc {
6227                line: tok.line,
6228                column: tok.column,
6229            },
6230            leading_trivia: Vec::new(),
6231            trailing_trivia: Vec::new(),
6232        };
6233        // Skip optional parameters/return type before brace
6234        while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
6235            self.advance();
6236        }
6237        self.consume(TokenType::LBrace)?;
6238        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6239            let field = self.current().clone();
6240            let field_name = field.value.clone();
6241            self.advance();
6242            if self.check(TokenType::Colon) {
6243                self.advance();
6244                match field_name.as_str() {
6245                    "goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
6246                    "tools" => node.tools = self.parse_bracketed_identifiers()?,
6247                    "memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
6248                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
6249                    "on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
6250                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
6251                    // §Fase 71.c — `window: <WindowName>` temporal binding.
6252                    "window" => node.window_ref = self.consume_any_ident_or_kw()?.value.clone(),
6253                    "max_tokens" => node.max_tokens = self.parse_optional_int(),
6254                    "max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
6255                    "max_cost" => node.max_cost = self.parse_optional_float(),
6256                    // §Fase 52.d — `requires: [cap, …]` capability scope (same
6257                    // closed slug grammar as `axonendpoint requires:`). The
6258                    // enterprise supervisor mints a per-run principal scoped to
6259                    // exactly these (least privilege).
6260                    "requires" => {
6261                        let bracket_tok = self.current().clone();
6262                        let items = self.parse_bracketed_dot_identifiers()?;
6263                        for slug in &items {
6264                            if !is_valid_capability_slug(slug) {
6265                                return Err(ParseError {
6266                                    message: format!(
6267                                        "Invalid capability slug '{slug}' in daemon '{}' \
6268                                         `requires:`. Capability slugs must match \
6269                                         ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
6270                                         lowercase identifiers. Examples: `daemon.run`, \
6271                                         `memory.write`, `flow.execute`.",
6272                                        node.name
6273                                    ),
6274                                    line: bracket_tok.line,
6275                                    column: bracket_tok.column,
6276                                    ..Default::default()
6277                                });
6278                            }
6279                        }
6280                        node.requires_capabilities = items;
6281                    }
6282                    _ => self.skip_value(),
6283                }
6284            } else if field.ttype == TokenType::Listen {
6285                // §λ-L-E Fase 13 D4 — preserve listen blocks for type
6286                // checking.  We backtracked past the `listen` keyword
6287                // by `advance()` above, so reconstruct a synthetic
6288                // listener using the same dual-mode dispatch the flow
6289                // step parser uses (string topic OR typed channel ref).
6290                let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
6291                    (self.consume(TokenType::StringLit)?.value.clone(), false)
6292                } else {
6293                    (self.consume_any_ident_or_kw()?.value.clone(), true)
6294                };
6295                let mut alias = String::new();
6296                if !self.at_declaration_start()
6297                    && !self.check(TokenType::RBrace)
6298                    && !self.check(TokenType::LBrace)
6299                {
6300                    let next = self.current().clone();
6301                    if next.value == "as" || next.ttype == TokenType::As {
6302                        self.advance();
6303                        alias = self.consume_any_ident_or_kw()?.value.clone();
6304                    }
6305                }
6306                let listen_loc = Loc {
6307                    line: field.line,
6308                    column: field.column,
6309                };
6310                // §Fase 52.a — parse the handler body (was skipped). This is
6311                // what makes a `daemon` operational: the body runs per event /
6312                // scheduled tick (e.g. a `listen "cron:…" as tick { run … }`).
6313                let body = self.parse_listener_body()?;
6314                node.listeners.push(ListenStep {
6315                    channel,
6316                    channel_is_ref,
6317                    event_alias: alias,
6318                    body,
6319                    loc: listen_loc,
6320                });
6321            } else if field_name == "budget" && self.check(TokenType::LBrace) {
6322                // §Fase 72.a — the `budget { … }` linear-effect rate-limit block.
6323                node.budget = Some(self.parse_budget_block(field.line, field.column)?);
6324            } else if self.check(TokenType::LBrace) {
6325                self.skip_braced_block()?;
6326            }
6327        }
6328        self.consume(TokenType::RBrace)?;
6329        Ok(node)
6330    }
6331
6332    /// §Fase 72.a — `budget { <rate|max>: N per <period> on Tool(<X>) … [on_exhausted: <p>] }`.
6333    fn parse_budget_block(&mut self, line: u32, column: u32) -> Result<BudgetBlock, ParseError> {
6334        self.consume(TokenType::LBrace)?;
6335        let mut quotas = Vec::new();
6336        let mut on_exhausted = String::new();
6337        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6338            let field = self.current().clone();
6339            let field_name = self.consume_any_ident_or_kw()?.value;
6340            match field_name.as_str() {
6341                "rate" | "max" => {
6342                    quotas.push(self.parse_budget_quota(field_name, field.line, field.column)?);
6343                }
6344                "on_exhausted" => {
6345                    self.consume(TokenType::Colon)?;
6346                    on_exhausted = self.consume_any_ident_or_kw()?.value;
6347                }
6348                _ => self.skip_value(),
6349            }
6350        }
6351        self.consume(TokenType::RBrace)?;
6352        Ok(BudgetBlock {
6353            quotas,
6354            on_exhausted,
6355            loc: Loc { line, column },
6356        })
6357    }
6358
6359    /// §Fase 72.a — one quota line: `<kind>: <limit> per <period> on Tool(<effect>)`.
6360    /// `kind` (`rate`/`max`) is already consumed by the caller.
6361    fn parse_budget_quota(
6362        &mut self,
6363        kind: String,
6364        line: u32,
6365        column: u32,
6366    ) -> Result<BudgetQuota, ParseError> {
6367        self.consume(TokenType::Colon)?;
6368        let limit = self.consume_number()? as i64;
6369        // `per <period>`
6370        let _per = self.consume_any_ident_or_kw()?; // the `per` keyword
6371        let period = self.consume_any_ident_or_kw()?.value;
6372        // `on Tool(<effect>)`
6373        let _on = self.consume_any_ident_or_kw()?; // the `on` keyword
6374        let _tool = self.consume_any_ident_or_kw()?; // the `Tool` wrapper keyword
6375        self.consume(TokenType::LParen)?;
6376        let effect = self.consume_any_ident_or_kw()?.value;
6377        self.consume(TokenType::RParen)?;
6378        Ok(BudgetQuota {
6379            kind,
6380            limit,
6381            period,
6382            effect,
6383            loc: Loc { line, column },
6384        })
6385    }
6386
6387    fn parse_axonstore(&mut self) -> Result<AxonStoreDefinition, ParseError> {
6388        let tok = self.consume(TokenType::AxonStore)?;
6389        let name = self.consume(TokenType::Identifier)?.value;
6390        let mut node = AxonStoreDefinition {
6391            name,
6392            backend: String::new(),
6393            connection: String::new(),
6394            confidence_floor: None,
6395            isolation: String::new(),
6396            on_breach: String::new(),
6397            capability: String::new(),
6398            class: String::new(),
6399            column_schema: None,
6400            loc: Loc {
6401                line: tok.line,
6402                column: tok.column,
6403            },
6404            leading_trivia: Vec::new(),
6405            trailing_trivia: Vec::new(),
6406        };
6407        self.consume(TokenType::LBrace)?;
6408        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6409            let field = self.current().clone();
6410            let field_name = field.value.clone();
6411            // §Fase 38.b (D1) — `schema:` declaration in three closed
6412            // forms: inline column block, manifest reference (string
6413            // literal), or env-var schema namespace (`env:VAR` —
6414            // unquoted or quoted). Parse the form; the §38.d / §38.e
6415            // type-checker consumes the resulting AST.
6416            if field.ttype == TokenType::Schema {
6417                self.advance();
6418                let parsed = self.parse_store_schema_declaration(&node.name, field.line, field.column)?;
6419                node.column_schema = Some(parsed);
6420                continue;
6421            }
6422            self.advance();
6423            if self.check(TokenType::Colon) {
6424                self.advance();
6425                match field_name.as_str() {
6426                    "backend" => node.backend = self.consume_any_ident_or_kw()?.value.clone(),
6427                    // §Fase 94.a — the secret-class prefix of a
6428                    // `backend: secrets` metadata store. Dotted-identifier
6429                    // form (`class: crm`, `class: crm.oauth`); the
6430                    // secrets-only placement rule + slug shape are
6431                    // `axon-T900` in the type-checker (it needs the
6432                    // resolved `backend:`, which may appear after this
6433                    // field in source order).
6434                    "class" => node.class = self.parse_dotted_identifier()?,
6435                    "connection" => {
6436                        node.connection = self.consume(TokenType::StringLit)?.value.clone()
6437                    }
6438                    "confidence_floor" => node.confidence_floor = self.parse_optional_float(),
6439                    "isolation" => node.isolation = self.consume_any_ident_or_kw()?.value.clone(),
6440                    "on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
6441                    // §Fase 35.j (D11) — Pillar IV: the capability slug
6442                    // required to access this store. Validated against
6443                    // the closed slug grammar shared with `requires:`.
6444                    "capability" => {
6445                        let slug_tok = self.consume(TokenType::StringLit)?.clone();
6446                        if !is_valid_capability_slug(&slug_tok.value) {
6447                            return Err(ParseError {
6448                                message: format!(
6449                                    "Invalid capability slug '{}' in axonstore '{}' \
6450                                     `capability:`. Capability slugs must match \
6451                                     ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
6452                                     lowercase identifiers starting with a letter. Examples: \
6453                                     `admin`, `tenant.read`, `hipaa.phi.read`.",
6454                                    slug_tok.value, node.name
6455                                ),
6456                                line: slug_tok.line,
6457                                column: slug_tok.column,
6458                                ..Default::default()
6459                            });
6460                        }
6461                        node.capability = slug_tok.value.clone();
6462                    }
6463                    _ => self.skip_value(),
6464                }
6465            } else if self.check(TokenType::LBrace) {
6466                self.skip_braced_block()?;
6467            }
6468        }
6469        self.consume(TokenType::RBrace)?;
6470        Ok(node)
6471    }
6472
6473    /// §Fase 38.b (D1) — parse the three closed forms of an `axonstore`
6474    /// `schema:` declaration:
6475    ///
6476    ///   * form (a) **inline** — `schema { col: Type [constraint…], … }`
6477    ///   * form (b) **manifest reference** — `schema: "qualified.name"`
6478    ///     (string literal that does NOT start with `env:`)
6479    ///   * form (c) **env-var schema namespace** — `schema: env:VAR`
6480    ///     (unquoted) OR `schema: "env:VAR"` (quoted; the literal
6481    ///     starts with `env:`)
6482    ///
6483    /// Called immediately AFTER `schema` is consumed.
6484    fn parse_store_schema_declaration(
6485        &mut self,
6486        store_name: &str,
6487        sch_line: u32,
6488        sch_col: u32,
6489    ) -> Result<crate::store_schema::StoreColumnSchema, ParseError> {
6490        use crate::store_schema::{StoreColumn, StoreColumnSchema, StoreColumnType};
6491
6492        // — Form (a) — inline column block: `schema { ... }`. —
6493        if self.check(TokenType::LBrace) {
6494            self.consume(TokenType::LBrace)?;
6495            let mut columns: Vec<StoreColumn> = Vec::new();
6496            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6497                let col_tok = self.current().clone();
6498                let col_name = self.consume_any_ident_or_kw()?.value.clone();
6499                self.consume(TokenType::Colon)?;
6500                let type_tok = self.consume_any_ident_or_kw()?.clone();
6501                let col_type = StoreColumnType::from_token(&type_tok.value).ok_or_else(|| {
6502                    let names = StoreColumnType::all_canonical_names();
6503                    let suggestion =
6504                        crate::smart_suggest::suggest_for(&type_tok.value, &names);
6505                    let suggest_suffix = if suggestion.is_empty() {
6506                        String::new()
6507                    } else {
6508                        format!(" {suggestion}")
6509                    };
6510                    let known = names.join(", ");
6511                    ParseError {
6512                        message: format!(
6513                            "Unknown column type `{}` for column `{}` in \
6514                             axonstore `{}` `schema:` block. The closed \
6515                             v1.38.0 column-type catalog (Fase 38.b D1) \
6516                             is {{{known}}} (plus common lowercase \
6517                             aliases — `int`/`integer`/`int4` for \
6518                             `Int`, `bool`/`boolean` for `Bool`, etc.).\
6519                             {suggest_suffix}",
6520                            type_tok.value, col_name, store_name
6521                        ),
6522                        line: type_tok.line,
6523                        column: type_tok.column,
6524                        ..Default::default()
6525                    }
6526                })?;
6527
6528                // §Fase 73.a (D1) — the OPTIONAL `Json<T>` shape LENS on a
6529                // column. `payload: Json<UserEvent>` records the expected
6530                // struct shape; the lens is a compile-time expectation only
6531                // (the column stays physically `jsonb`, navigated totally at
6532                // runtime — doctrine `open_data_is_total`). The shape's
6533                // well-formedness (T is a declared `type`) is `axon-T840`
6534                // in the type-checker — it needs the symbol table. Here we
6535                // only enforce the STRUCTURAL rule: a `<T>` lens may refine
6536                // ONLY a `Json` / `Jsonb` column — `axon-T841` otherwise.
6537                let mut json_shape: Option<String> = None;
6538                if self.check(TokenType::Lt) {
6539                    self.advance();
6540                    let shape_tok = self.consume_any_ident_or_kw()?.clone();
6541                    self.consume(TokenType::Gt)?;
6542                    if matches!(col_type, StoreColumnType::Json | StoreColumnType::Jsonb) {
6543                        json_shape = Some(shape_tok.value.clone());
6544                    } else {
6545                        return Err(ParseError {
6546                            message: format!(
6547                                "axon-T841 a shape lens `<{shape}>` may refine \
6548                                 only a `Json` / `Jsonb` column, but column \
6549                                 `{col}` in axonstore `{store}` is `{ty}`. Drop \
6550                                 the `<{shape}>` (a rigid column already has a \
6551                                 fixed shape), or change the column type to \
6552                                 `Json<{shape}>` if it carries open documents.",
6553                                shape = shape_tok.value,
6554                                col = col_name,
6555                                store = store_name,
6556                                ty = col_type.canonical_name(),
6557                            ),
6558                            line: shape_tok.line,
6559                            column: shape_tok.column,
6560                            ..Default::default()
6561                        });
6562                    }
6563                }
6564
6565                let mut col = StoreColumn {
6566                    name: col_name,
6567                    col_type,
6568                    json_shape,
6569                    primary_key: false,
6570                    auto_increment: false,
6571                    not_null: false,
6572                    unique: false,
6573                    indexed: false,
6574                    default_value: String::new(),
6575                    // §Fase 38.x.d (D1) — `identity` is now a recognized
6576                    // inline keyword (see the constraint loop below).
6577                    // Defaults to false; set to true when the adopter
6578                    // writes `id: BigInt primary_key identity`.
6579                    identity: false,
6580                    line: col_tok.line,
6581                    column: col_tok.column,
6582                };
6583
6584                // Trailing constraints (position-independent), matching
6585                // the Python `_parse_store_column` surface.
6586                while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6587                    if self.current().ttype != TokenType::Identifier {
6588                        // The next column starts with a non-identifier
6589                        // (rare) — stop the constraint scan.
6590                        break;
6591                    }
6592                    let constraint = self.current().value.clone();
6593                    match constraint.as_str() {
6594                        "primary_key" => {
6595                            col.primary_key = true;
6596                            self.advance();
6597                        }
6598                        "auto_increment" => {
6599                            col.auto_increment = true;
6600                            self.advance();
6601                        }
6602                        "not_null" => {
6603                            col.not_null = true;
6604                            self.advance();
6605                        }
6606                        "unique" => {
6607                            col.unique = true;
6608                            self.advance();
6609                        }
6610                        // §Fase 73.f (D1) — the `index` constraint declares
6611                        // an index as a capability-honest effect (visible to
6612                        // the deploy gate, not a silent DBA action). The
6613                        // backend picks the method from the column type
6614                        // (GIN for a Json/Jsonb column, b-tree otherwise).
6615                        "index" => {
6616                            col.indexed = true;
6617                            self.advance();
6618                        }
6619                        // §Fase 38.x.d (D1) — `identity` marks a column
6620                        // as `GENERATED ALWAYS/BY DEFAULT AS IDENTITY`.
6621                        // Distinct from `auto_increment` (legacy SERIAL
6622                        // via `nextval(...)` default). T803 skips
6623                        // identity columns from the NOT-NULL-omission
6624                        // check because Postgres auto-fills them; the
6625                        // distinction matters because IDENTITY ALWAYS
6626                        // also rejects user-supplied values, where
6627                        // SERIAL accepts them (a future 38.x.e arm in
6628                        // T802 may surface this).
6629                        "identity" => {
6630                            col.identity = true;
6631                            self.advance();
6632                        }
6633                        "default" => {
6634                            self.advance();
6635                            let dv = self.current().clone();
6636                            if matches!(
6637                                dv.ttype,
6638                                TokenType::StringLit
6639                                    | TokenType::Integer
6640                                    | TokenType::Float
6641                            ) {
6642                                col.default_value = dv.value.clone();
6643                                self.advance();
6644                            } else {
6645                                col.default_value =
6646                                    self.consume_any_ident_or_kw()?.value.clone();
6647                            }
6648                        }
6649                        _ => break,
6650                    }
6651                }
6652
6653                columns.push(col);
6654            }
6655            self.consume(TokenType::RBrace)?;
6656            return Ok(StoreColumnSchema::Inline {
6657                columns,
6658                leading_trivia: Vec::new(),
6659                line: sch_line,
6660                column: sch_col,
6661            });
6662        }
6663
6664        // — Forms (b) + (c) require a `:` separator. —
6665        if !self.check(TokenType::Colon) {
6666            let cur = self.current().clone();
6667            return Err(ParseError {
6668                message: format!(
6669                    "axonstore `{store_name}` `schema:` declaration expects \
6670                     `{{ … }}` (inline columns), `: \"manifest.ref\"` \
6671                     (manifest reference), or `: env:VAR` (per-tenant schema \
6672                     namespace). Got `{}` instead.",
6673                    cur.value
6674                ),
6675                line: cur.line,
6676                column: cur.column,
6677                ..Default::default()
6678            });
6679        }
6680        self.consume(TokenType::Colon)?;
6681
6682        // — Form (b) or (c)-quoted — string literal value. —
6683        if self.check(TokenType::StringLit) {
6684            let lit = self.consume(TokenType::StringLit)?.clone();
6685            let value = lit.value.clone();
6686            if let Some(var) = value.strip_prefix("env:") {
6687                let var = var.trim();
6688                if var.is_empty() {
6689                    return Err(ParseError {
6690                        message: format!(
6691                            "axonstore `{store_name}` `schema: \"env:\"` is \
6692                             missing the variable name after the `env:` \
6693                             prefix."
6694                        ),
6695                        line: lit.line,
6696                        column: lit.column,
6697                        ..Default::default()
6698                    });
6699                }
6700                return Ok(StoreColumnSchema::EnvVar {
6701                    var_name: var.to_string(),
6702                    line: sch_line,
6703                    column: sch_col,
6704                });
6705            }
6706            // Plain string → manifest reference.
6707            if value.trim().is_empty() {
6708                return Err(ParseError {
6709                    message: format!(
6710                        "axonstore `{store_name}` `schema:` manifest reference \
6711                         is empty. Expected `\"qualified.name\"` — e.g. \
6712                         `\"public.tenants\"`."
6713                    ),
6714                    line: lit.line,
6715                    column: lit.column,
6716                    ..Default::default()
6717                });
6718            }
6719            return Ok(StoreColumnSchema::ManifestRef {
6720                qualified_name: value,
6721                line: sch_line,
6722                column: sch_col,
6723            });
6724        }
6725
6726        // — Form (c) unquoted — `env:VAR`. The lexer emits `env` as an
6727        //   identifier, then `:`, then the identifier var name. —
6728        let env_tok = self.current().clone();
6729        if env_tok.value == "env" {
6730            self.advance();
6731            if !self.check(TokenType::Colon) {
6732                return Err(ParseError {
6733                    message: format!(
6734                        "axonstore `{store_name}` `schema: env` is missing the \
6735                         `:` separator. Expected `schema: env:VAR`."
6736                    ),
6737                    line: env_tok.line,
6738                    column: env_tok.column,
6739                    ..Default::default()
6740                });
6741            }
6742            self.advance(); // past ':'
6743            let var_tok = self.consume_any_ident_or_kw()?.clone();
6744            if var_tok.value.trim().is_empty() {
6745                return Err(ParseError {
6746                    message: format!(
6747                        "axonstore `{store_name}` `schema: env:` is missing \
6748                         the variable name."
6749                    ),
6750                    line: var_tok.line,
6751                    column: var_tok.column,
6752                    ..Default::default()
6753                });
6754            }
6755            return Ok(StoreColumnSchema::EnvVar {
6756                var_name: var_tok.value.clone(),
6757                line: sch_line,
6758                column: sch_col,
6759            });
6760        }
6761
6762        Err(ParseError {
6763            message: format!(
6764                "axonstore `{store_name}` `schema:` declaration expects \
6765                 `{{ … }}` (inline columns), `\"manifest.ref\"` (manifest \
6766                 reference), or `env:VAR` (per-tenant schema namespace). \
6767                 Got `{}` instead.",
6768                env_tok.value
6769            ),
6770            line: env_tok.line,
6771            column: env_tok.column,
6772            ..Default::default()
6773        })
6774    }
6775
6776    // ── §λ-L-E Fase 1 — Resource primitive ────────────────────────
6777
6778    /// Parse: `resource Name { kind, endpoint, capacity, lifetime, certainty_floor, shield }`.
6779    ///
6780    /// Mirrors `axon.compiler.parser.Parser._parse_resource`. Unknown fields
6781    /// are silently skipped (keeps the grammar forward-compatible).
6782    fn parse_resource(&mut self) -> Result<ResourceDefinition, ParseError> {
6783        let tok = self.consume(TokenType::Resource)?;
6784        let name = self.consume(TokenType::Identifier)?.value;
6785        let mut node = ResourceDefinition {
6786            name,
6787            kind: String::new(),
6788            endpoint: String::new(),
6789            capacity: None,
6790            lifetime: "affine".to_string(),
6791            certainty_floor: None,
6792            shield_ref: String::new(),
6793            loc: Loc {
6794                line: tok.line,
6795                column: tok.column,
6796            },
6797            leading_trivia: Vec::new(),
6798            trailing_trivia: Vec::new(),
6799        };
6800        self.consume(TokenType::LBrace)?;
6801        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6802            let field_tok = self.current().clone();
6803            let field_name = field_tok.value.clone();
6804            self.advance();
6805            if !self.check(TokenType::Colon) {
6806                // Tolerate stray brace or unknown layout.
6807                if self.check(TokenType::LBrace) {
6808                    self.skip_braced_block()?;
6809                }
6810                continue;
6811            }
6812            self.advance(); // past ':'
6813            match field_name.as_str() {
6814                "kind" => node.kind = self.consume_any_ident_or_kw()?.value,
6815                "endpoint" => node.endpoint = self.consume(TokenType::StringLit)?.value,
6816                "capacity" => {
6817                    node.capacity = self.parse_optional_int();
6818                }
6819                "lifetime" => {
6820                    let lt_tok = self.consume_any_ident_or_kw()?;
6821                    let lt = lt_tok.value;
6822                    if !matches!(lt.as_str(), "linear" | "affine" | "persistent") {
6823                        return Err(ParseError {
6824                            message: format!(
6825                                "Invalid lifetime '{lt}' in resource '{}' — \
6826                                 expected linear | affine | persistent",
6827                                node.name
6828                            ),
6829                            line: lt_tok.line,
6830                            column: lt_tok.column,
6831                                                    ..Default::default()
6832                        });
6833                    }
6834                    node.lifetime = lt;
6835                }
6836                "certainty_floor" => {
6837                    node.certainty_floor = self.parse_optional_float();
6838                }
6839                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
6840                _ => self.skip_value(),
6841            }
6842        }
6843        self.consume(TokenType::RBrace)?;
6844        Ok(node)
6845    }
6846
6847    /// Parse: `fabric Name { provider, region, zones, ephemeral, shield }`.
6848    fn parse_fabric(&mut self) -> Result<FabricDefinition, ParseError> {
6849        let tok = self.consume(TokenType::Fabric)?;
6850        let name = self.consume(TokenType::Identifier)?.value;
6851        let mut node = FabricDefinition {
6852            name,
6853            provider: String::new(),
6854            region: String::new(),
6855            zones: None,
6856            ephemeral: None,
6857            shield_ref: String::new(),
6858            loc: Loc {
6859                line: tok.line,
6860                column: tok.column,
6861            },
6862            leading_trivia: Vec::new(),
6863            trailing_trivia: Vec::new(),
6864        };
6865        self.consume(TokenType::LBrace)?;
6866        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6867            let field_name = self.current().value.clone();
6868            self.advance();
6869            if !self.check(TokenType::Colon) {
6870                if self.check(TokenType::LBrace) {
6871                    self.skip_braced_block()?;
6872                }
6873                continue;
6874            }
6875            self.advance(); // past ':'
6876            match field_name.as_str() {
6877                "provider" => node.provider = self.consume_any_ident_or_kw()?.value,
6878                "region" => node.region = self.consume(TokenType::StringLit)?.value,
6879                "zones" => node.zones = self.parse_optional_int(),
6880                "ephemeral" => {
6881                    let b = self.parse_bool()?;
6882                    node.ephemeral = Some(b);
6883                }
6884                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
6885                _ => self.skip_value(),
6886            }
6887        }
6888        self.consume(TokenType::RBrace)?;
6889        Ok(node)
6890    }
6891
6892    /// Parse: `manifest Name { resources, fabric, region, zones, compliance }`.
6893    fn parse_manifest(&mut self) -> Result<ManifestDefinition, ParseError> {
6894        let tok = self.consume(TokenType::Manifest)?;
6895        let name = self.consume(TokenType::Identifier)?.value;
6896        let mut node = ManifestDefinition {
6897            name,
6898            resources: Vec::new(),
6899            fabric_ref: String::new(),
6900            region: String::new(),
6901            zones: None,
6902            compliance: Vec::new(),
6903            loc: Loc {
6904                line: tok.line,
6905                column: tok.column,
6906            },
6907            leading_trivia: Vec::new(),
6908            trailing_trivia: Vec::new(),
6909        };
6910        self.consume(TokenType::LBrace)?;
6911        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6912            let field_name = self.current().value.clone();
6913            self.advance();
6914            if !self.check(TokenType::Colon) {
6915                if self.check(TokenType::LBrace) {
6916                    self.skip_braced_block()?;
6917                }
6918                continue;
6919            }
6920            self.advance();
6921            match field_name.as_str() {
6922                "resources" => node.resources = self.parse_bracketed_identifiers()?,
6923                "fabric" => node.fabric_ref = self.consume_any_ident_or_kw()?.value,
6924                "region" => node.region = self.consume(TokenType::StringLit)?.value,
6925                "zones" => node.zones = self.parse_optional_int(),
6926                "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
6927                _ => self.skip_value(),
6928            }
6929        }
6930        self.consume(TokenType::RBrace)?;
6931        Ok(node)
6932    }
6933
6934    /// Parse: `observe Name from Manifest { sources, quorum, timeout, on_partition, certainty_floor }`.
6935    fn parse_observe(&mut self) -> Result<ObserveDefinition, ParseError> {
6936        let tok = self.consume(TokenType::Observe)?;
6937        let name = self.consume(TokenType::Identifier)?.value;
6938        // `from <Manifest>` — required per Python grammar.
6939        self.consume(TokenType::From)?;
6940        let target = self.consume(TokenType::Identifier)?.value;
6941        let mut node = ObserveDefinition {
6942            name,
6943            target,
6944            sources: Vec::new(),
6945            quorum: None,
6946            timeout: String::new(),
6947            on_partition: "fail".to_string(),
6948            certainty_floor: None,
6949            loc: Loc {
6950                line: tok.line,
6951                column: tok.column,
6952            },
6953            leading_trivia: Vec::new(),
6954            trailing_trivia: Vec::new(),
6955        };
6956        self.consume(TokenType::LBrace)?;
6957        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6958            let field_name = self.current().value.clone();
6959            self.advance();
6960            if !self.check(TokenType::Colon) {
6961                if self.check(TokenType::LBrace) {
6962                    self.skip_braced_block()?;
6963                }
6964                continue;
6965            }
6966            self.advance();
6967            match field_name.as_str() {
6968                "sources" => node.sources = self.parse_bracketed_identifiers()?,
6969                "quorum" => node.quorum = self.parse_optional_int(),
6970                "timeout" => {
6971                    let t = self.current().clone();
6972                    match t.ttype {
6973                        TokenType::Duration | TokenType::StringLit => {
6974                            self.advance();
6975                            node.timeout = t.value;
6976                        }
6977                        _ => node.timeout = self.consume_any_ident_or_kw()?.value,
6978                    }
6979                }
6980                "on_partition" => {
6981                    let p_tok = self.consume_any_ident_or_kw()?;
6982                    let p = p_tok.value;
6983                    if !matches!(p.as_str(), "fail" | "shield_quarantine") {
6984                        return Err(ParseError {
6985                            message: format!(
6986                                "Invalid on_partition '{p}' in observe '{}' — \
6987                                 expected fail | shield_quarantine",
6988                                node.name
6989                            ),
6990                            line: p_tok.line,
6991                            column: p_tok.column,
6992                                                    ..Default::default()
6993                        });
6994                    }
6995                    node.on_partition = p;
6996                }
6997                "certainty_floor" => node.certainty_floor = self.parse_optional_float(),
6998                _ => self.skip_value(),
6999            }
7000        }
7001        self.consume(TokenType::RBrace)?;
7002        Ok(node)
7003    }
7004
7005    // ── §λ-L-E Fase 3 — Control cognitivo ─────────────────────────
7006
7007    /// Parse: `reconcile Name { observe, threshold, tolerance, on_drift, shield, mandate, max_retries }`.
7008    fn parse_reconcile(&mut self) -> Result<ReconcileDefinition, ParseError> {
7009        let tok = self.consume(TokenType::Reconcile)?;
7010        let name = self.consume(TokenType::Identifier)?.value;
7011        let mut node = ReconcileDefinition {
7012            name,
7013            observe_ref: String::new(),
7014            threshold: None,
7015            tolerance: None,
7016            on_drift: "provision".to_string(),
7017            shield_ref: String::new(),
7018            mandate_ref: String::new(),
7019            max_retries: 3,
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                if self.check(TokenType::LBrace) {
7033                    self.skip_braced_block()?;
7034                }
7035                continue;
7036            }
7037            self.advance();
7038            match field_name.as_str() {
7039                "observe" => node.observe_ref = self.consume_any_ident_or_kw()?.value,
7040                "threshold" => node.threshold = self.parse_optional_float(),
7041                "tolerance" => node.tolerance = self.parse_optional_float(),
7042                "on_drift" => {
7043                    let d_tok = self.consume_any_ident_or_kw()?;
7044                    let d = d_tok.value;
7045                    if !matches!(d.as_str(), "provision" | "alert" | "refine") {
7046                        return Err(ParseError {
7047                            message: format!(
7048                                "Invalid on_drift '{d}' in reconcile '{}' — \
7049                                 expected provision | alert | refine",
7050                                node.name
7051                            ),
7052                            line: d_tok.line,
7053                            column: d_tok.column,
7054                                                    ..Default::default()
7055                        });
7056                    }
7057                    node.on_drift = d;
7058                }
7059                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
7060                "mandate" => node.mandate_ref = self.consume_any_ident_or_kw()?.value,
7061                "max_retries" => {
7062                    if let Some(v) = self.parse_optional_int() {
7063                        node.max_retries = v;
7064                    }
7065                }
7066                _ => self.skip_value(),
7067            }
7068        }
7069        self.consume(TokenType::RBrace)?;
7070        Ok(node)
7071    }
7072
7073    /// Parse: `lease Name { resource, duration, acquire, on_expire }`.
7074    fn parse_lease(&mut self) -> Result<LeaseDefinition, ParseError> {
7075        let tok = self.consume(TokenType::Lease)?;
7076        let name = self.consume(TokenType::Identifier)?.value;
7077        let mut node = LeaseDefinition {
7078            name,
7079            resource_ref: String::new(),
7080            duration: String::new(),
7081            acquire: "on_start".to_string(),
7082            on_expire: "anchor_breach".to_string(),
7083            loc: Loc {
7084                line: tok.line,
7085                column: tok.column,
7086            },
7087            leading_trivia: Vec::new(),
7088            trailing_trivia: Vec::new(),
7089        };
7090        self.consume(TokenType::LBrace)?;
7091        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7092            let field_name = self.current().value.clone();
7093            self.advance();
7094            if !self.check(TokenType::Colon) {
7095                if self.check(TokenType::LBrace) {
7096                    self.skip_braced_block()?;
7097                }
7098                continue;
7099            }
7100            self.advance();
7101            match field_name.as_str() {
7102                "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
7103                "duration" => {
7104                    let t = self.current().clone();
7105                    match t.ttype {
7106                        TokenType::Duration | TokenType::StringLit => {
7107                            self.advance();
7108                            node.duration = t.value;
7109                        }
7110                        _ => node.duration = self.consume_any_ident_or_kw()?.value,
7111                    }
7112                }
7113                "acquire" => {
7114                    let a_tok = self.consume_any_ident_or_kw()?;
7115                    let a = a_tok.value;
7116                    if !matches!(a.as_str(), "on_start" | "on_demand") {
7117                        return Err(ParseError {
7118                            message: format!(
7119                                "Invalid acquire '{a}' in lease '{}' — \
7120                                 expected on_start | on_demand",
7121                                node.name
7122                            ),
7123                            line: a_tok.line,
7124                            column: a_tok.column,
7125                                                    ..Default::default()
7126                        });
7127                    }
7128                    node.acquire = a;
7129                }
7130                "on_expire" => {
7131                    let e_tok = self.consume_any_ident_or_kw()?;
7132                    let e = e_tok.value;
7133                    if !matches!(e.as_str(), "anchor_breach" | "release" | "extend") {
7134                        return Err(ParseError {
7135                            message: format!(
7136                                "Invalid on_expire '{e}' in lease '{}' — \
7137                                 expected anchor_breach | release | extend",
7138                                node.name
7139                            ),
7140                            line: e_tok.line,
7141                            column: e_tok.column,
7142                                                    ..Default::default()
7143                        });
7144                    }
7145                    node.on_expire = e;
7146                }
7147                _ => self.skip_value(),
7148            }
7149        }
7150        self.consume(TokenType::RBrace)?;
7151        Ok(node)
7152    }
7153
7154    /// Parse: `ensemble Name { observations, quorum, aggregation, certainty_mode }`.
7155    fn parse_ensemble(&mut self) -> Result<EnsembleDefinition, ParseError> {
7156        let tok = self.consume(TokenType::Ensemble)?;
7157        let name = self.consume(TokenType::Identifier)?.value;
7158        let mut node = EnsembleDefinition {
7159            name,
7160            observations: Vec::new(),
7161            quorum: None,
7162            aggregation: "majority".to_string(),
7163            certainty_mode: "min".to_string(),
7164            loc: Loc {
7165                line: tok.line,
7166                column: tok.column,
7167            },
7168            leading_trivia: Vec::new(),
7169            trailing_trivia: Vec::new(),
7170        };
7171        self.consume(TokenType::LBrace)?;
7172        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7173            let field_name = self.current().value.clone();
7174            self.advance();
7175            if !self.check(TokenType::Colon) {
7176                if self.check(TokenType::LBrace) {
7177                    self.skip_braced_block()?;
7178                }
7179                continue;
7180            }
7181            self.advance();
7182            match field_name.as_str() {
7183                "observations" => node.observations = self.parse_bracketed_identifiers()?,
7184                "quorum" => node.quorum = self.parse_optional_int(),
7185                "aggregation" => {
7186                    let a_tok = self.consume_any_ident_or_kw()?;
7187                    let a = a_tok.value;
7188                    if !matches!(a.as_str(), "majority" | "weighted" | "byzantine") {
7189                        return Err(ParseError {
7190                            message: format!(
7191                                "Invalid aggregation '{a}' in ensemble '{}' — \
7192                                 expected majority | weighted | byzantine",
7193                                node.name
7194                            ),
7195                            line: a_tok.line,
7196                            column: a_tok.column,
7197                                                    ..Default::default()
7198                        });
7199                    }
7200                    node.aggregation = a;
7201                }
7202                "certainty_mode" => {
7203                    let c_tok = self.consume_any_ident_or_kw()?;
7204                    let c = c_tok.value;
7205                    if !matches!(c.as_str(), "min" | "weighted" | "harmonic") {
7206                        return Err(ParseError {
7207                            message: format!(
7208                                "Invalid certainty_mode '{c}' in ensemble '{}' — \
7209                                 expected min | weighted | harmonic",
7210                                node.name
7211                            ),
7212                            line: c_tok.line,
7213                            column: c_tok.column,
7214                                                    ..Default::default()
7215                        });
7216                    }
7217                    node.certainty_mode = c;
7218                }
7219                _ => self.skip_value(),
7220            }
7221        }
7222        self.consume(TokenType::RBrace)?;
7223        Ok(node)
7224    }
7225
7226    // ── §λ-L-E Fase 4 — Topology + π-calculus binary sessions ─────
7227
7228    /// Parse: `session Name { role1: [step, …]  role2: [step, …] }`.
7229    ///
7230    /// The enclosing `parse_session_definition` disambiguates from the session
7231    /// step token `session` (which does not exist) by always entering from the
7232    /// top-level dispatch; the identifier role name is consumed after `{`.
7233    fn parse_session_definition(&mut self) -> Result<SessionDefinition, ParseError> {
7234        let tok = self.consume(TokenType::Session)?;
7235        let name = self.consume(TokenType::Identifier)?.value;
7236        let mut node = SessionDefinition {
7237            name,
7238            roles: Vec::new(),
7239            loc: Loc {
7240                line: tok.line,
7241                column: tok.column,
7242            },
7243            leading_trivia: Vec::new(),
7244            trailing_trivia: Vec::new(),
7245        };
7246        self.consume(TokenType::LBrace)?;
7247        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7248            let role_tok = self.consume_any_ident_or_kw()?;
7249            self.consume(TokenType::Colon)?;
7250            let steps = self.parse_session_steps()?;
7251            node.roles.push(SessionRole {
7252                name: role_tok.value,
7253                steps,
7254                loc: Loc {
7255                    line: role_tok.line,
7256                    column: role_tok.column,
7257                },
7258            });
7259        }
7260        self.consume(TokenType::RBrace)?;
7261        Ok(node)
7262    }
7263
7264    /// §Fase 51.c.2 — Parse a Pauli-sum observable declaration:
7265    /// ```text
7266    /// observable EnergyHamiltonian {
7267    ///     qubits: 2
7268    ///     term: 0.5 * "ZZ"
7269    ///     term: -1.2 * "XI"
7270    /// }
7271    /// ```
7272    /// `term:` is a repeatable key (one `cₖ · Pₖ` per line). The coefficient is
7273    /// a real scalar (optional leading `+`/`-`), then `*`, then a quoted Pauli
7274    /// string. The type-checker (§51.c.2) validates the closed `{I,X,Y,Z}`
7275    /// alphabet + equal lengths; real coefficients ⇒ Hermitian by construction.
7276    fn parse_observable(&mut self) -> Result<ObservableDefinition, ParseError> {
7277        let tok = self.consume(TokenType::Observable)?;
7278        let name = self.consume(TokenType::Identifier)?.value;
7279        let mut node = ObservableDefinition {
7280            name,
7281            qubits: None,
7282            terms: Vec::new(),
7283            loc: Loc {
7284                line: tok.line,
7285                column: tok.column,
7286            },
7287            leading_trivia: Vec::new(),
7288            trailing_trivia: Vec::new(),
7289        };
7290        self.consume(TokenType::LBrace)?;
7291        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7292            let key_tok = self.consume_any_ident_or_kw()?;
7293            self.consume(TokenType::Colon)?;
7294            match key_tok.value.as_str() {
7295                "qubits" => node.qubits = Some(self.consume_number()? as i64),
7296                "term" => {
7297                    let term_loc = Loc {
7298                        line: key_tok.line,
7299                        column: key_tok.column,
7300                    };
7301                    // Optional sign, then magnitude.
7302                    let mut negative = false;
7303                    if self.check(TokenType::Minus) {
7304                        self.advance();
7305                        negative = true;
7306                    } else if self.check(TokenType::Plus) {
7307                        self.advance();
7308                    }
7309                    let mag = self.consume_number()?;
7310                    let coefficient = if negative { -mag } else { mag };
7311                    // `*` separator between coefficient and Pauli string.
7312                    self.consume(TokenType::Star)?;
7313                    let pauli = self.consume(TokenType::StringLit)?.value;
7314                    node.terms.push(PauliTerm {
7315                        coefficient,
7316                        pauli,
7317                        loc: term_loc,
7318                    });
7319                }
7320                _ => self.skip_value(),
7321            }
7322        }
7323        self.consume(TokenType::RBrace)?;
7324        Ok(node)
7325    }
7326
7327    /// §Fase 69.a — Parse:
7328    /// `witness Name { claim: <ref>  against: <baseline>  metric: <metric>
7329    ///                 threshold: <ε>  data: <source> }`.
7330    /// Order-free `key: value` pairs. `claim`/`against`/`metric`/`data` are bare
7331    /// identifiers (a ref or a closed-catalog keyword); `threshold` is a number.
7332    /// Well-formedness (known metric, threshold range, required fields) is the
7333    /// type-checker's job (`axon-E0790`).
7334    fn parse_witness(&mut self) -> Result<WitnessDefinition, ParseError> {
7335        let tok = self.consume(TokenType::Witness)?;
7336        let name = self.consume(TokenType::Identifier)?.value;
7337        let mut node = WitnessDefinition {
7338            name,
7339            claim: String::new(),
7340            baseline: String::new(),
7341            metric: String::new(),
7342            threshold: 0.0,
7343            data: String::new(),
7344            loc: Loc {
7345                line: tok.line,
7346                column: tok.column,
7347            },
7348            leading_trivia: Vec::new(),
7349            trailing_trivia: Vec::new(),
7350        };
7351        self.consume(TokenType::LBrace)?;
7352        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7353            let key_tok = self.consume_any_ident_or_kw()?;
7354            self.consume(TokenType::Colon)?;
7355            match key_tok.value.as_str() {
7356                "claim" => node.claim = self.consume_any_ident_or_kw()?.value,
7357                // `against` is the baseline; `against` is not a reserved keyword,
7358                // so it lexes as an identifier key here.
7359                "against" => node.baseline = self.consume_any_ident_or_kw()?.value,
7360                "metric" => node.metric = self.consume_any_ident_or_kw()?.value,
7361                "threshold" => node.threshold = self.consume_number()?,
7362                "data" => node.data = self.consume_any_ident_or_kw()?.value,
7363                _ => self.skip_value(),
7364            }
7365        }
7366        self.consume(TokenType::RBrace)?;
7367        Ok(node)
7368    }
7369
7370    /// §Fase 41.b — Parse:
7371    /// `socket Name { protocol: SessionRef, backpressure: credit(n),
7372    ///               reconnect: cognitive_state, legal_basis: ... }`.
7373    /// Fields are `key: value` pairs (order-free); only `protocol` is required.
7374    fn parse_socket(&mut self) -> Result<SocketDefinition, ParseError> {
7375        let tok = self.consume(TokenType::Socket)?;
7376        let name = self.consume(TokenType::Identifier)?.value;
7377        let mut node = SocketDefinition {
7378            name,
7379            loc: Loc { line: tok.line, column: tok.column },
7380            ..Default::default()
7381        };
7382        self.consume(TokenType::LBrace)?;
7383        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7384            let key = self.consume_any_ident_or_kw()?.value;
7385            self.consume(TokenType::Colon)?;
7386            match key.as_str() {
7387                "protocol" => node.protocol = self.consume_any_ident_or_kw()?.value,
7388                "backpressure" => {
7389                    // `credit(n)` — the typed-resource window.
7390                    let kind = self.consume_any_ident_or_kw()?.value;
7391                    if kind != "credit" {
7392                        return Err(self.error(&format!("expected `credit(n)` for backpressure, got `{kind}`")));
7393                    }
7394                    self.consume(TokenType::LParen)?;
7395                    let n = self
7396                        .consume(TokenType::Integer)?
7397                        .value
7398                        .parse::<i64>()
7399                        .map_err(|_| self.error("backpressure credit must be an integer"))?;
7400                    self.consume(TokenType::RParen)?;
7401                    node.backpressure_credit = Some(n);
7402                }
7403                "reconnect" => {
7404                    let mode = self.consume_any_ident_or_kw()?.value;
7405                    node.reconnect = mode == "cognitive_state";
7406                }
7407                "legal_basis" => node.legal_basis = Some(self.consume_any_ident_or_kw()?.value),
7408                other => return Err(self.error(&format!("unknown socket field `{other}`"))),
7409            }
7410            // Optional comma between fields.
7411            if self.check(TokenType::Comma) {
7412                self.consume(TokenType::Comma)?;
7413            }
7414        }
7415        self.consume(TokenType::RBrace)?;
7416        Ok(node)
7417    }
7418
7419    /// §Fase 80.b — parse `upstream Name [from Preset@vN] { fields }`.
7420    ///
7421    /// Field grammar per `docs/fase/fase_80_upstream_design.md` §1–2. The
7422    /// parser fixes the *shape* only; catalog membership (`transport:`,
7423    /// `auth:`, `overflow:`, `on_exhausted:`), key charsets and projection
7424    /// totality are §80.c type-checker laws (T849–T851), mirroring how
7425    /// `socket` splits parse vs. check.
7426    fn parse_upstream(&mut self) -> Result<UpstreamDefinition, ParseError> {
7427        let tok = self.consume(TokenType::Upstream)?;
7428        let name = self.consume(TokenType::Identifier)?.value;
7429        let mut node = UpstreamDefinition {
7430            name,
7431            loc: Loc { line: tok.line, column: tok.column },
7432            ..Default::default()
7433        };
7434        // §80.f — preset instantiation: `upstream X from DeepgramSTT@v1 {…}`.
7435        if self.check(TokenType::From) {
7436            self.advance();
7437            let base = self.consume(TokenType::Identifier)?.value;
7438            self.consume(TokenType::At)?;
7439            let version = self.consume_any_ident_or_kw()?.value;
7440            node.preset = Some(format!("{base}@{version}"));
7441        }
7442        self.consume(TokenType::LBrace)?;
7443        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7444            let key = self.consume_any_ident_or_kw()?.value;
7445            self.consume(TokenType::Colon)?;
7446            match key.as_str() {
7447                "transport" => node.transport = self.consume_any_ident_or_kw()?.value,
7448                "protocol" => node.protocol = self.consume_any_ident_or_kw()?.value,
7449                "role" => node.role = self.consume_any_ident_or_kw()?.value,
7450                "resolve" => node.resolve = self.parse_dotted_identifier()?,
7451                "secret" => node.secret = self.parse_dotted_identifier()?,
7452                "auth" => {
7453                    // `header("Name")` | `header("Name", "Prefix ")` |
7454                    // `query("param")` | `signed_url`.
7455                    node.auth_kind = self.consume_any_ident_or_kw()?.value;
7456                    if self.check(TokenType::LParen) {
7457                        self.consume(TokenType::LParen)?;
7458                        node.auth_name = Some(self.consume(TokenType::StringLit)?.value);
7459                        if self.check(TokenType::Comma) {
7460                            self.consume(TokenType::Comma)?;
7461                            node.auth_prefix = Some(self.consume(TokenType::StringLit)?.value);
7462                        }
7463                        self.consume(TokenType::RParen)?;
7464                    }
7465                }
7466                "map" => node.map = self.parse_upstream_map()?,
7467                "reconnect" => node.reconnect = Some(self.parse_upstream_reconnect()?),
7468                "overflow" => node.overflow = Some(self.consume_any_ident_or_kw()?.value),
7469                "backpressure" => {
7470                    // `credit(n)` — same typed-resource window as `socket`.
7471                    let kind = self.consume_any_ident_or_kw()?.value;
7472                    if kind != "credit" {
7473                        return Err(self.error(&format!("expected `credit(n)` for backpressure, got `{kind}`")));
7474                    }
7475                    self.consume(TokenType::LParen)?;
7476                    let n = self
7477                        .consume(TokenType::Integer)?
7478                        .value
7479                        .parse::<i64>()
7480                        .map_err(|_| self.error("backpressure credit must be an integer"))?;
7481                    self.consume(TokenType::RParen)?;
7482                    node.backpressure_credit = Some(n);
7483                }
7484                other => return Err(self.error(&format!("unknown upstream field `{other}`"))),
7485            }
7486            // Optional comma between fields.
7487            if self.check(TokenType::Comma) {
7488                self.consume(TokenType::Comma)?;
7489            }
7490        }
7491        self.consume(TokenType::RBrace)?;
7492        Ok(node)
7493    }
7494
7495    /// §Fase 83.a — parse `cors Name { fields }`. Field-shape checks
7496    /// (wildcard+credentials, origin-glob shape, closed method catalog,
7497    /// cross-method path consistency) are §83.c type-checker territory
7498    /// (T853-T857); the parser only builds the structural AST.
7499    ///
7500    /// **Unknown fields are a hard error** (D83.7, not `shield`'s lenient
7501    /// `axon-W010` record-and-skip) — mirrors `upstream`'s stricter
7502    /// posture, appropriate for a security-relevant declaration.
7503    fn parse_cors(&mut self) -> Result<CorsDefinition, ParseError> {
7504        let tok = self.consume(TokenType::Cors)?;
7505        let name = self.consume(TokenType::Identifier)?.value;
7506        let mut node = CorsDefinition {
7507            name,
7508            loc: Loc { line: tok.line, column: tok.column },
7509            ..Default::default()
7510        };
7511        self.consume(TokenType::LBrace)?;
7512        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7513            let key = self.consume_any_ident_or_kw()?.value;
7514            self.consume(TokenType::Colon)?;
7515            match key.as_str() {
7516                "allow_origins" => node.allow_origins = self.parse_bracketed_strings()?,
7517                "allow_methods" => node.allow_methods = self.parse_bracketed_identifiers()?,
7518                "allow_headers" => node.allow_headers = self.parse_bracketed_strings()?,
7519                "allow_credentials" => {
7520                    node.allow_credentials = self.consume_any_ident_or_kw()?.value == "true"
7521                }
7522                "max_age" => node.max_age = Some(self.consume(TokenType::Duration)?.value),
7523                "expose_headers" => node.expose_headers = self.parse_bracketed_strings()?,
7524                other => return Err(self.error(&format!("unknown cors field `{other}`"))),
7525            }
7526            // Optional comma between fields.
7527            if self.check(TokenType::Comma) {
7528                self.consume(TokenType::Comma)?;
7529            }
7530        }
7531        self.consume(TokenType::RBrace)?;
7532        Ok(node)
7533    }
7534
7535    /// §Fase 92.a — parse `credential Name { ttl: grants: }`. Strict
7536    /// closed-catalog (unknown field is a hard error, the §83 D83.7
7537    /// discipline — a credential contract governs AUTHORITY, so a typo can
7538    /// never silently produce a permissive contract). `grants:` slugs are
7539    /// validated at parse time with the same closed grammar as
7540    /// `axonendpoint requires:`; the cross-field laws (non-empty grants,
7541    /// TTL bounds) are §92.a type-checker territory (`axon-T893`/`T894`).
7542    fn parse_credential(&mut self) -> Result<CredentialDefinition, ParseError> {
7543        let tok = self.consume(TokenType::Credential)?;
7544        let name = self.consume(TokenType::Identifier)?.value;
7545        let mut node = CredentialDefinition {
7546            name,
7547            loc: Loc { line: tok.line, column: tok.column },
7548            ..Default::default()
7549        };
7550        self.consume(TokenType::LBrace)?;
7551        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7552            let key = self.consume_any_ident_or_kw()?.value;
7553            self.consume(TokenType::Colon)?;
7554            match key.as_str() {
7555                "ttl" => node.ttl = self.consume(TokenType::Duration)?.value,
7556                "grants" => {
7557                    let bracket_tok = self.current().clone();
7558                    let items = self.parse_bracketed_dot_identifiers()?;
7559                    for slug in &items {
7560                        if !is_valid_capability_slug(slug) {
7561                            return Err(ParseError {
7562                                message: format!(
7563                                    "Invalid capability slug '{slug}' in credential '{}' \
7564                                     `grants:`. Capability slugs must match \
7565                                     ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
7566                                     lowercase identifiers starting with a letter. Examples: \
7567                                     `chat.invoke`, `flow.execute`.",
7568                                    node.name
7569                                ),
7570                                line: bracket_tok.line,
7571                                column: bracket_tok.column,
7572                                ..Default::default()
7573                            });
7574                        }
7575                    }
7576                    node.grants = items;
7577                }
7578                other => return Err(self.error(&format!("unknown credential field `{other}`"))),
7579            }
7580            // Optional comma between fields.
7581            if self.check(TokenType::Comma) {
7582                self.consume(TokenType::Comma)?;
7583            }
7584        }
7585        self.consume(TokenType::RBrace)?;
7586        Ok(node)
7587    }
7588
7589    /// §Fase 85.a — parse `cache Name { backend:, ttl:, key:, default:,
7590    /// apply_to_effects:, invalidate_on: }`. Strict closed-catalog (unknown
7591    /// field is a hard error, the §83 D83.7 discipline — a cache governs
7592    /// correctness, so a typo can never silently mean "no policy"). All
7593    /// cross-field laws (single default, non-pure-needs-ttl, reference
7594    /// resolution, effect widening) are §85.c type-checker territory.
7595    fn parse_cache(&mut self) -> Result<CacheDefinition, ParseError> {
7596        let tok = self.consume(TokenType::Cache)?;
7597        let name = self.consume(TokenType::Identifier)?.value;
7598        let mut node = CacheDefinition {
7599            name,
7600            loc: Loc { line: tok.line, column: tok.column },
7601            ..Default::default()
7602        };
7603        self.consume(TokenType::LBrace)?;
7604        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7605            let key = self.consume_any_ident_or_kw()?.value;
7606            self.consume(TokenType::Colon)?;
7607            match key.as_str() {
7608                "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
7609                "ttl" => node.ttl = Some(self.consume(TokenType::Duration)?.value),
7610                "key" => node.key_params = self.parse_bracketed_identifiers()?,
7611                "default" => {
7612                    node.default_policy = self.consume_any_ident_or_kw()?.value == "true"
7613                }
7614                "apply_to_effects" => {
7615                    node.apply_to_effects = self.parse_bracketed_identifiers()?
7616                }
7617                "invalidate_on" => node.invalidate_on = self.parse_bracketed_identifiers()?,
7618                other => return Err(self.error(&format!("unknown cache field `{other}`"))),
7619            }
7620            if self.check(TokenType::Comma) {
7621                self.consume(TokenType::Comma)?;
7622            }
7623        }
7624        self.consume(TokenType::RBrace)?;
7625        Ok(node)
7626    }
7627
7628    // ── §Fase 99.b — Native Document Synthesis ─────────────────────────────
7629
7630    /// §Fase 99.b — parse `document <Name> { target:, template:?, provenance:?,
7631    /// effects:?, <body blocks> }`. Document-level scalars are handled here;
7632    /// anything of the form `ident { … }` is a body block ([`parse_doc_block_body`]).
7633    /// Unknown scalar fields are a hard error (the §83/§84 closed-catalog
7634    /// discipline); the per-`target` block vocabulary is the §99.c checker's job.
7635    fn parse_document(&mut self) -> Result<crate::ast::DocumentDefinition, ParseError> {
7636        let tok = self.consume(TokenType::Document)?;
7637        let name = self.consume(TokenType::Identifier)?.value;
7638        let mut node = crate::ast::DocumentDefinition {
7639            name,
7640            loc: Loc {
7641                line: tok.line,
7642                column: tok.column,
7643            },
7644            ..Default::default()
7645        };
7646        self.consume(TokenType::LBrace)?;
7647        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7648            let field = self.current().clone();
7649            let field_name = field.value.clone();
7650            self.advance();
7651            if self.check(TokenType::Colon) {
7652                self.advance();
7653                match field_name.as_str() {
7654                    "target" => node.target = self.consume_any_ident_or_kw()?.value,
7655                    "template" => node.template = self.parse_dotted_identifier()?,
7656                    "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
7657                    "effects" => node.effects = Some(self.parse_effect_row()?),
7658                    other => {
7659                        return Err(self.error(&format!(
7660                            "unknown document field `{other}` in document `{}` — expected \
7661                             `target:` / `template:` / `provenance:` / `effects:`, or a body \
7662                             block (`section {{ … }}` / `slide {{ … }}` / `sheet {{ … }}`)",
7663                            node.name
7664                        )))
7665                    }
7666                }
7667            } else if self.check(TokenType::LBrace) {
7668                node.blocks
7669                    .push(self.parse_doc_block_body(field_name, field.line, field.column)?);
7670            } else {
7671                return Err(self.error(&format!(
7672                    "unexpected `{field_name}` in document `{}` body — expected a `field:` or a \
7673                     body block `{field_name} {{ … }}`",
7674                    node.name
7675                )));
7676            }
7677            if self.check(TokenType::Comma) {
7678                self.advance();
7679            }
7680        }
7681        self.consume(TokenType::RBrace)?;
7682        Ok(node)
7683    }
7684
7685    /// §Fase 99.b — parse a document body block whose `kind` was already
7686    /// consumed: `{ (field: value | nested-block { … })* }`. Recursive — a
7687    /// `section` holds `para`/`table`/`chart`; a `slide` holds `bullets`/
7688    /// `notes`; a `sheet` holds `row`/`formula`. A member is a field iff a
7689    /// `:` follows its name; else it must open a nested block (`{`).
7690    fn parse_doc_block_body(
7691        &mut self,
7692        kind: String,
7693        line: u32,
7694        column: u32,
7695    ) -> Result<crate::ast::DocBlock, ParseError> {
7696        let mut block = crate::ast::DocBlock {
7697            kind,
7698            loc: Loc { line, column },
7699            ..Default::default()
7700        };
7701        self.consume(TokenType::LBrace)?;
7702        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7703            let name_tok = self.current().clone();
7704            let name = self.consume_any_ident_or_kw()?.value;
7705            if self.check(TokenType::Colon) {
7706                self.advance();
7707                let value = self.parse_doc_scalar()?;
7708                block.fields.push((name, value));
7709            } else if self.check(TokenType::LBrace) {
7710                let child = self.parse_doc_block_body(name, name_tok.line, name_tok.column)?;
7711                block.children.push(child);
7712            } else {
7713                return Err(self.error(&format!(
7714                    "in document block `{}`: `{name}` must be a `field:` value or open a nested \
7715                     block `{name} {{ … }}`",
7716                    block.kind
7717                )));
7718            }
7719            if self.check(TokenType::Comma) {
7720                self.advance();
7721            }
7722        }
7723        self.consume(TokenType::RBrace)?;
7724        Ok(block)
7725    }
7726
7727    /// §Fase 99.b — parse a document field value into a [`crate::ast::DocScalar`].
7728    /// A bare identifier is a REFERENCE (`text: revenue_summary`) — this is what
7729    /// the assertion-laundering barrier inspects; a quoted string / int / bool /
7730    /// bracketed list are literals.
7731    fn parse_doc_scalar(&mut self) -> Result<crate::ast::DocScalar, ParseError> {
7732        let tok = self.current().clone();
7733        match tok.ttype {
7734            TokenType::StringLit => {
7735                self.advance();
7736                Ok(crate::ast::DocScalar::Text(tok.value))
7737            }
7738            TokenType::Integer => {
7739                self.advance();
7740                Ok(crate::ast::DocScalar::Int(tok.value.parse::<i64>().unwrap_or(0)))
7741            }
7742            TokenType::Bool => {
7743                self.advance();
7744                Ok(crate::ast::DocScalar::Bool(tok.value == "true"))
7745            }
7746            TokenType::LBracket => {
7747                let items = self.parse_bracketed_strings()?;
7748                Ok(crate::ast::DocScalar::List(items))
7749            }
7750            _ => {
7751                let name = self.consume_any_ident_or_kw()?.value;
7752                Ok(crate::ast::DocScalar::Ref(name))
7753            }
7754        }
7755    }
7756
7757    // ── §Fase 105 — Governed CRM Delivery ──────────────────────────────────
7758
7759    /// §Fase 105 — parse `deliver <Name> { target:, provenance:?, secret:,
7760    /// effects:?, <operation blocks> }`. Delivery-level scalars are handled here;
7761    /// anything of the form `ident { … }` is an operation block
7762    /// ([`parse_deliver_op`]). Unknown scalar fields are a hard error (the §99
7763    /// closed-catalog discipline); the operation vocabulary is the checker's job.
7764    fn parse_deliver(&mut self) -> Result<crate::ast::DeliverDefinition, ParseError> {
7765        let tok = self.consume(TokenType::Deliver)?;
7766        let name = self.consume(TokenType::Identifier)?.value;
7767        let mut node = crate::ast::DeliverDefinition {
7768            name,
7769            loc: Loc {
7770                line: tok.line,
7771                column: tok.column,
7772            },
7773            ..Default::default()
7774        };
7775        self.consume(TokenType::LBrace)?;
7776        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7777            let field = self.current().clone();
7778            let field_name = field.value.clone();
7779            self.advance();
7780            if self.check(TokenType::Colon) {
7781                self.advance();
7782                match field_name.as_str() {
7783                    "target" => node.target = self.consume_any_ident_or_kw()?.value,
7784                    "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
7785                    "secret" => node.secret = self.consume_any_ident_or_kw()?.value,
7786                    "effects" => node.effects = Some(self.parse_effect_row()?),
7787                    other => {
7788                        return Err(self.error(&format!(
7789                            "unknown deliver field `{other}` in deliver `{}` — expected \
7790                             `target:` / `provenance:` / `secret:` / `effects:`, or an operation \
7791                             block (`upsert_contact {{ … }}` / `create_deal {{ … }}` / \
7792                             `add_note {{ … }}`)",
7793                            node.name
7794                        )))
7795                    }
7796                }
7797            } else if self.check(TokenType::LBrace) {
7798                node.ops
7799                    .push(self.parse_deliver_op(field_name, field.line, field.column)?);
7800            } else {
7801                return Err(self.error(&format!(
7802                    "unexpected `{field_name}` in deliver `{}` body — expected a `field:` or an \
7803                     operation block `{field_name} {{ … }}`",
7804                    node.name
7805                )));
7806            }
7807            if self.check(TokenType::Comma) {
7808                self.advance();
7809            }
7810        }
7811        self.consume(TokenType::RBrace)?;
7812        Ok(node)
7813    }
7814
7815    /// §Fase 105 — parse a delivery operation block whose `kind` was already
7816    /// consumed: `{ (field: value)* }`. Flat (unlike a document block, an
7817    /// operation has no nested children) — each member must be a `field: value`.
7818    fn parse_deliver_op(
7819        &mut self,
7820        kind: String,
7821        line: u32,
7822        column: u32,
7823    ) -> Result<crate::ast::DeliverOp, ParseError> {
7824        let mut op = crate::ast::DeliverOp {
7825            kind,
7826            loc: Loc { line, column },
7827            ..Default::default()
7828        };
7829        self.consume(TokenType::LBrace)?;
7830        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7831            let name = self.consume_any_ident_or_kw()?.value;
7832            self.consume(TokenType::Colon).map_err(|_| {
7833                self.error(&format!(
7834                    "in deliver operation `{}`: `{name}` must be a `field: value` pair — an \
7835                     operation binds scalar fields, it takes no nested blocks",
7836                    op.kind
7837                ))
7838            })?;
7839            let value = self.parse_doc_scalar()?;
7840            op.fields.push((name, value));
7841            if self.check(TokenType::Comma) {
7842                self.advance();
7843            }
7844        }
7845        self.consume(TokenType::RBrace)?;
7846        Ok(op)
7847    }
7848
7849    /// §Fase 87.a — parse `savant <Name> { domain:, cognition{…}, memory{…},
7850    /// budget{…}, mandate <M> {…} … }`. The block surface only; catalog +
7851    /// ref-resolution + budget/interruptibility binding is the §87.b/c checker's
7852    /// job (the standing parse/check split). Unknown fields are a hard error
7853    /// (D83.7): a savant governs an expensive autonomous process.
7854    fn parse_savant(&mut self) -> Result<SavantDefinition, ParseError> {
7855        let tok = self.consume(TokenType::Savant)?;
7856        let name = self.consume(TokenType::Identifier)?.value;
7857        let mut node = SavantDefinition {
7858            name,
7859            loc: Loc {
7860                line: tok.line,
7861                column: tok.column,
7862            },
7863            ..Default::default()
7864        };
7865        self.consume(TokenType::LBrace)?;
7866        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7867            let field = self.current().clone();
7868            let field_name = field.value.clone();
7869            self.advance();
7870            if self.check(TokenType::Colon) {
7871                self.advance();
7872                match field_name.as_str() {
7873                    "domain" => node.domain = self.consume(TokenType::StringLit)?.value,
7874                    other => {
7875                        return Err(self.error(&format!(
7876                            "unknown savant field `{other}` in savant `{}` — expected \
7877                             `domain:` or a `cognition` / `memory` / `budget` / `mandate` block",
7878                            node.name
7879                        )))
7880                    }
7881                }
7882            } else if field_name == "cognition" {
7883                node.cognition = Some(self.parse_savant_cognition(field.line, field.column)?);
7884            } else if field_name == "memory" {
7885                node.memory = Some(self.parse_savant_memory(field.line, field.column)?);
7886            } else if field_name == "budget" {
7887                node.budget = Some(self.parse_savant_budget(field.line, field.column)?);
7888            } else if field_name == "mandate" {
7889                node.mandates
7890                    .push(self.parse_savant_mandate(field.line, field.column)?);
7891            } else {
7892                return Err(self.error(&format!(
7893                    "unexpected `{field_name}` in savant `{}` body — expected `domain:` or a \
7894                     `cognition` / `memory` / `budget` / `mandate` block",
7895                    node.name
7896                )));
7897            }
7898            if self.check(TokenType::Comma) {
7899                self.advance();
7900            }
7901        }
7902        self.consume(TokenType::RBrace)?;
7903        Ok(node)
7904    }
7905
7906    /// §Fase 87.a — the `cognition { depth:, entropic_threshold:, divergence: }`
7907    /// sub-block. Catalog validation of `depth`/`divergence` is §87.b.
7908    fn parse_savant_cognition(
7909        &mut self,
7910        line: u32,
7911        column: u32,
7912    ) -> Result<SavantCognition, ParseError> {
7913        self.consume(TokenType::LBrace)?;
7914        let mut node = SavantCognition {
7915            loc: Loc { line, column },
7916            ..Default::default()
7917        };
7918        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7919            let key = self.consume_any_ident_or_kw()?.value;
7920            self.consume(TokenType::Colon)?;
7921            match key.as_str() {
7922                "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
7923                "entropic_threshold" => node.entropic_threshold = self.parse_optional_float(),
7924                "divergence" => node.divergence = self.consume_any_ident_or_kw()?.value,
7925                other => {
7926                    return Err(self.error(&format!(
7927                        "unknown savant `cognition` field `{other}` — expected \
7928                         `depth` / `entropic_threshold` / `divergence`"
7929                    )))
7930                }
7931            }
7932            if self.check(TokenType::Comma) {
7933                self.advance();
7934            }
7935        }
7936        self.consume(TokenType::RBrace)?;
7937        Ok(node)
7938    }
7939
7940    /// §Fase 87.a — the `memory { backend:, corpus_graph:, isolation_level: }`
7941    /// sub-block. `backend` is resolved to a declared `memory`/`corpus` in §87.c.
7942    fn parse_savant_memory(
7943        &mut self,
7944        line: u32,
7945        column: u32,
7946    ) -> Result<SavantMemory, ParseError> {
7947        self.consume(TokenType::LBrace)?;
7948        let mut node = SavantMemory {
7949            loc: Loc { line, column },
7950            ..Default::default()
7951        };
7952        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7953            let key = self.consume_any_ident_or_kw()?.value;
7954            self.consume(TokenType::Colon)?;
7955            match key.as_str() {
7956                "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
7957                "corpus_graph" => {
7958                    node.corpus_graph = self.consume_any_ident_or_kw()?.value == "true"
7959                }
7960                "isolation_level" => node.isolation_level = self.consume_any_ident_or_kw()?.value,
7961                other => {
7962                    return Err(self.error(&format!(
7963                        "unknown savant `memory` field `{other}` — expected \
7964                         `backend` / `corpus_graph` / `isolation_level`"
7965                    )))
7966                }
7967            }
7968            if self.check(TokenType::Comma) {
7969                self.advance();
7970            }
7971        }
7972        self.consume(TokenType::RBrace)?;
7973        Ok(node)
7974    }
7975
7976    /// §Fase 87.a — the `budget { max_iterations:, max_tool_synth: }` sub-block.
7977    /// Bound to a §72 linear budget (`RateLease`) in §87.c.
7978    fn parse_savant_budget(
7979        &mut self,
7980        line: u32,
7981        column: u32,
7982    ) -> Result<SavantBudget, ParseError> {
7983        self.consume(TokenType::LBrace)?;
7984        let mut node = SavantBudget {
7985            loc: Loc { line, column },
7986            ..Default::default()
7987        };
7988        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7989            let key = self.consume_any_ident_or_kw()?.value;
7990            self.consume(TokenType::Colon)?;
7991            match key.as_str() {
7992                "max_iterations" => node.max_iterations = self.parse_optional_int(),
7993                "max_tool_synth" => node.max_tool_synth = self.parse_optional_int(),
7994                other => {
7995                    return Err(self.error(&format!(
7996                        "unknown savant `budget` field `{other}` — expected \
7997                         `max_iterations` / `max_tool_synth`"
7998                    )))
7999                }
8000            }
8001            if self.check(TokenType::Comma) {
8002                self.advance();
8003            }
8004        }
8005        self.consume(TokenType::RBrace)?;
8006        Ok(node)
8007    }
8008
8009    /// §Fase 87.a — the `mandate <Name> { objective:, output: }` sub-block. The
8010    /// `mandate` keyword is already consumed by `parse_savant`.
8011    fn parse_savant_mandate(
8012        &mut self,
8013        line: u32,
8014        column: u32,
8015    ) -> Result<SavantMandate, ParseError> {
8016        let name = self.consume(TokenType::Identifier)?.value;
8017        let mut node = SavantMandate {
8018            name,
8019            loc: Loc { line, column },
8020            ..Default::default()
8021        };
8022        self.consume(TokenType::LBrace)?;
8023        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8024            let key = self.consume_any_ident_or_kw()?.value;
8025            self.consume(TokenType::Colon)?;
8026            match key.as_str() {
8027                "objective" => node.objective = self.consume(TokenType::StringLit)?.value,
8028                "output" => node.output_type = self.consume_any_ident_or_kw()?.value,
8029                other => {
8030                    return Err(self.error(&format!(
8031                        "unknown savant `mandate` field `{other}` — expected `objective` / `output`"
8032                    )))
8033                }
8034            }
8035            if self.check(TokenType::Comma) {
8036                self.advance();
8037            }
8038        }
8039        self.consume(TokenType::RBrace)?;
8040        Ok(node)
8041    }
8042
8043    /// §Fase 87.d — parse `synth <Name> { target:, risk:, language:, sandbox:,
8044    /// review:, max_lines: }`. Flat key:value block (the `cache` shape). Catalog
8045    /// + deny-by-default validation is §87.d `check_synth`. Unknown fields are a
8046    /// hard error (D83.7): a synth policy governs arbitrary-code execution.
8047    fn parse_synth(&mut self) -> Result<SynthDefinition, ParseError> {
8048        let tok = self.consume(TokenType::Synth)?;
8049        let name = self.consume(TokenType::Identifier)?.value;
8050        let mut node = SynthDefinition {
8051            name,
8052            loc: Loc {
8053                line: tok.line,
8054                column: tok.column,
8055            },
8056            ..Default::default()
8057        };
8058        self.consume(TokenType::LBrace)?;
8059        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8060            let key = self.consume_any_ident_or_kw()?.value;
8061            self.consume(TokenType::Colon)?;
8062            match key.as_str() {
8063                "target" => node.target = self.consume(TokenType::StringLit)?.value,
8064                "risk" => node.risk = self.consume_any_ident_or_kw()?.value,
8065                "language" => node.language = self.consume_any_ident_or_kw()?.value,
8066                "sandbox" => node.sandbox = self.consume_any_ident_or_kw()?.value,
8067                "review" => node.review = self.consume_any_ident_or_kw()?.value,
8068                "max_lines" => node.max_lines = self.parse_optional_int(),
8069                other => {
8070                    return Err(self.error(&format!(
8071                        "unknown synth field `{other}` in synth `{}` — expected `target` / `risk` \
8072                         / `language` / `sandbox` / `review` / `max_lines`",
8073                        node.name
8074                    )))
8075                }
8076            }
8077            if self.check(TokenType::Comma) {
8078                self.consume(TokenType::Comma)?;
8079            }
8080        }
8081        self.consume(TokenType::RBrace)?;
8082        Ok(node)
8083    }
8084
8085    /// §Fase 80.g — parse `voice Name { fields }`. Cross-field laws
8086    /// (stt/tts XOR realtime, interruptible ⇒ legal_basis, ref resolution)
8087    /// are §80.c type-checker territory (T852), same parse/check split as
8088    /// every primitive in this file.
8089    fn parse_voice(&mut self) -> Result<VoiceDefinition, ParseError> {
8090        let tok = self.consume(TokenType::Voice)?;
8091        let name = self.consume(TokenType::Identifier)?.value;
8092        let mut node = VoiceDefinition {
8093            name,
8094            loc: Loc { line: tok.line, column: tok.column },
8095            ..Default::default()
8096        };
8097        self.consume(TokenType::LBrace)?;
8098        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8099            let key = self.consume_any_ident_or_kw()?.value;
8100            self.consume(TokenType::Colon)?;
8101            match key.as_str() {
8102                // Each leg: a declared upstream name or a `Preset@vN` ref.
8103                "stt" => node.stt = Some(self.parse_upstream_ref()?),
8104                "tts" => node.tts = Some(self.parse_upstream_ref()?),
8105                "realtime" => node.realtime = Some(self.parse_upstream_ref()?),
8106                "carrier" => node.carrier = self.consume_any_ident_or_kw()?.value,
8107                "interruptible" => {
8108                    let v = self.consume_any_ident_or_kw()?.value;
8109                    node.interruptible = v == "true";
8110                }
8111                "legal_basis" => node.legal_basis = Some(self.consume_any_ident_or_kw()?.value),
8112                "persona" => node.persona = Some(self.consume(TokenType::Identifier)?.value),
8113                "context" => node.context = Some(self.consume(TokenType::Identifier)?.value),
8114                other => return Err(self.error(&format!("unknown voice field `{other}`"))),
8115            }
8116            if self.check(TokenType::Comma) {
8117                self.consume(TokenType::Comma)?;
8118            }
8119        }
8120        self.consume(TokenType::RBrace)?;
8121        Ok(node)
8122    }
8123
8124    /// §Fase 80.g — an upstream leg reference: `Ident` (a declared
8125    /// `upstream`) or `Ident@vN` (a §80.f preset).
8126    fn parse_upstream_ref(&mut self) -> Result<String, ParseError> {
8127        let base = self.consume(TokenType::Identifier)?.value;
8128        if self.check(TokenType::At) {
8129            self.advance();
8130            let version = self.consume_any_ident_or_kw()?.value;
8131            Ok(format!("{base}@{version}"))
8132        } else {
8133            Ok(base)
8134        }
8135    }
8136
8137    /// §Fase 80.b — parse the `map: [ rule, … ]` projection list.
8138    ///
8139    /// rule := (`send` | `receive`) <MessageType> `as` (`json` | `binary`)
8140    ///         [ `tag` <string> ]                 — send-json only
8141    ///         [ `when` <string> `=` <string> ]   — receive-json only
8142    fn parse_upstream_map(&mut self) -> Result<Vec<UpstreamMapRule>, ParseError> {
8143        self.consume(TokenType::LBracket)?;
8144        let mut rules = Vec::new();
8145        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
8146            let dir_tok = self.current().clone();
8147            let direction = match dir_tok.ttype {
8148                TokenType::Send => "send",
8149                TokenType::Receive => "receive",
8150                _ => {
8151                    return Err(self.error(&format!(
8152                        "upstream map rule must start with `send` or `receive`, got `{}`",
8153                        dir_tok.value
8154                    )))
8155                }
8156            };
8157            self.advance();
8158            let message = self.consume(TokenType::Identifier)?.value;
8159            self.consume(TokenType::As)?;
8160            let framing = self.consume_any_ident_or_kw()?.value;
8161            let mut rule = UpstreamMapRule {
8162                direction: direction.to_string(),
8163                message,
8164                framing,
8165                loc: Loc { line: dir_tok.line, column: dir_tok.column },
8166                ..Default::default()
8167            };
8168            // Optional selectors — contextual identifiers, not keywords.
8169            if self.current().value == "tag" {
8170                self.advance();
8171                rule.tag = Some(self.consume(TokenType::StringLit)?.value);
8172            } else if self.current().value == "when" {
8173                // `when "f" = "v"` — equality discriminator; `when "f"` —
8174                // field-PRESENCE discriminator (vendors like Gemini Live /
8175                // ElevenLabs mark frame kinds by which key exists, not by a
8176                // type value).
8177                self.advance();
8178                rule.when_field = Some(self.consume(TokenType::StringLit)?.value);
8179                if self.check(TokenType::Assign) {
8180                    self.advance();
8181                    rule.when_value = Some(self.consume(TokenType::StringLit)?.value);
8182                }
8183            }
8184            rules.push(rule);
8185            if self.check(TokenType::Comma) {
8186                self.advance();
8187            }
8188        }
8189        self.consume(TokenType::RBracket)?;
8190        Ok(rules)
8191    }
8192
8193    /// §Fase 80.b — parse `reconnect: { backoff_ms: <int>, max_attempts:
8194    /// <int>, on_exhausted: <ident> }` (order-free, all three required —
8195    /// a reconnection policy with a hole is not a policy).
8196    fn parse_upstream_reconnect(&mut self) -> Result<UpstreamReconnect, ParseError> {
8197        self.consume(TokenType::LBrace)?;
8198        let mut backoff_ms: Option<i64> = None;
8199        let mut max_attempts: Option<i64> = None;
8200        let mut on_exhausted: Option<String> = None;
8201        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8202            let key = self.consume_any_ident_or_kw()?.value;
8203            self.consume(TokenType::Colon)?;
8204            match key.as_str() {
8205                "backoff_ms" => {
8206                    backoff_ms = Some(
8207                        self.consume(TokenType::Integer)?
8208                            .value
8209                            .parse::<i64>()
8210                            .map_err(|_| self.error("backoff_ms must be an integer"))?,
8211                    )
8212                }
8213                "max_attempts" => {
8214                    max_attempts = Some(
8215                        self.consume(TokenType::Integer)?
8216                            .value
8217                            .parse::<i64>()
8218                            .map_err(|_| self.error("max_attempts must be an integer"))?,
8219                    )
8220                }
8221                "on_exhausted" => on_exhausted = Some(self.consume_any_ident_or_kw()?.value),
8222                other => return Err(self.error(&format!("unknown reconnect field `{other}`"))),
8223            }
8224            if self.check(TokenType::Comma) {
8225                self.consume(TokenType::Comma)?;
8226            }
8227        }
8228        self.consume(TokenType::RBrace)?;
8229        match (backoff_ms, max_attempts, on_exhausted) {
8230            (Some(b), Some(m), Some(o)) => Ok(UpstreamReconnect { backoff_ms: b, max_attempts: m, on_exhausted: o }),
8231            _ => Err(self.error(
8232                "reconnect requires all of `backoff_ms:`, `max_attempts:`, `on_exhausted:` — a reconnection policy with a hole is not a policy",
8233            )),
8234        }
8235    }
8236
8237    /// Parse: `[send T, receive U, loop, end]`.
8238    fn parse_session_steps(&mut self) -> Result<Vec<SessionStep>, ParseError> {
8239        self.consume(TokenType::LBracket)?;
8240        let mut steps = Vec::new();
8241        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
8242            steps.push(self.parse_session_step()?);
8243            if self.check(TokenType::Comma) {
8244                self.advance();
8245            }
8246        }
8247        self.consume(TokenType::RBracket)?;
8248        Ok(steps)
8249    }
8250
8251    /// §Fase 79.b — a **brace**-delimited session step block: `{ step, step, … }`.
8252    /// Used by the `interrupt`/`resumable` regions (the paper's block surface),
8253    /// as opposed to the `[ … ]` step-lists used by roles and choice arms.
8254    fn parse_session_step_block(&mut self) -> Result<Vec<SessionStep>, ParseError> {
8255        self.consume(TokenType::LBrace)?;
8256        let mut steps = Vec::new();
8257        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8258            steps.push(self.parse_session_step()?);
8259            if self.check(TokenType::Comma) {
8260                self.advance();
8261            }
8262        }
8263        self.consume(TokenType::RBrace)?;
8264        Ok(steps)
8265    }
8266
8267    fn parse_session_step(&mut self) -> Result<SessionStep, ParseError> {
8268        let tok = self.current().clone();
8269        let loc = Loc { line: tok.line, column: tok.column };
8270        match tok.ttype {
8271            TokenType::Send => {
8272                self.advance();
8273                let msg = self.consume_any_ident_or_kw()?;
8274                Ok(SessionStep { op: "send".into(), message_type: msg.value, loc, ..Default::default() })
8275            }
8276            TokenType::Receive => {
8277                self.advance();
8278                let msg = self.consume_any_ident_or_kw()?;
8279                Ok(SessionStep { op: "receive".into(), message_type: msg.value, loc, ..Default::default() })
8280            }
8281            TokenType::Loop => {
8282                self.advance();
8283                Ok(SessionStep { op: "loop".into(), loc, ..Default::default() })
8284            }
8285            TokenType::End => {
8286                self.advance();
8287                Ok(SessionStep { op: "end".into(), loc, ..Default::default() })
8288            }
8289            // §Fase 41.b — choice: `select { ℓ: [..], … }` (⊕) | `branch { ℓ: [..], … }` (&).
8290            // `select`/`branch` are not keywords — they arrive as identifiers.
8291            TokenType::Identifier if tok.value == "select" || tok.value == "branch" => {
8292                self.parse_session_choice(&tok.value, loc)
8293            }
8294            // §Fase 79.b — `interrupt { <body> } on <Signal> as <sig> resumable { <handler> }`.
8295            // Contextual keyword (identifier), like `select`/`branch`.
8296            TokenType::Identifier if tok.value == "interrupt" => {
8297                self.parse_session_interrupt(loc)
8298            }
8299            // §Fase 79.b — `resume`: the handler's normal exit (hand control back to
8300            // the parked body). A bare step, no payload; only meaningful inside an
8301            // `interrupt` handler (enforced at type-check, §79.c).
8302            TokenType::Identifier if tok.value == "resume" => {
8303                self.advance();
8304                Ok(SessionStep { op: "resume".into(), loc, ..Default::default() })
8305            }
8306            _ => Err(ParseError {
8307                message: format!(
8308                    "Invalid session step '{}' — expected send | receive | loop | end | select | branch | interrupt | resume",
8309                    tok.value
8310                ),
8311                line: tok.line,
8312                column: tok.column,
8313                ..Default::default()
8314            }),
8315        }
8316    }
8317
8318    /// §Fase 79.b — consume a **contextual keyword** (`on` / `as` / `resumable`):
8319    /// a token whose *value* must equal `kw`, regardless of whether the lexer
8320    /// classified it as a keyword or a bare identifier. Keeps the `interrupt`
8321    /// surface readable without minting three reserved words.
8322    fn consume_contextual(&mut self, kw: &str) -> Result<(), ParseError> {
8323        let t = self.current().clone();
8324        if t.value != kw {
8325            return Err(ParseError {
8326                message: format!("expected `{kw}` in interrupt step, got `{}`", t.value),
8327                line: t.line,
8328                column: t.column,
8329                ..Default::default()
8330            });
8331        }
8332        self.advance();
8333        Ok(())
8334    }
8335
8336    /// §Fase 79.b — Parse an interruptible region:
8337    /// `interrupt { <body-steps> } on <Signal> as <sig> resumable { <handler-steps> }`.
8338    ///
8339    /// Encoded into the string-tagged `SessionStep` (mirroring the §41.b choice
8340    /// shape): `op = "interrupt"`, `message_type = <Signal>` (validated against the
8341    /// closed `CallInterruptCause` catalog at type-check, §79.c), two labelled
8342    /// `branches` (`body`, `handler`), `binder = <sig>`, `resumable = true`.
8343    fn parse_session_interrupt(&mut self, loc: Loc) -> Result<SessionStep, ParseError> {
8344        self.advance(); // consume `interrupt`
8345        // Body region — a brace-delimited step block (the paper's `interrupt { … }`
8346        // surface; distinct from the `[ … ]` step-lists of roles/choice arms).
8347        let body = self.parse_session_step_block()?;
8348        // `on <Signal>`
8349        self.consume_contextual("on")?;
8350        let signal = self.consume_any_ident_or_kw()?;
8351        // `as <sig>`
8352        self.consume_contextual("as")?;
8353        let binder = self.consume_any_ident_or_kw()?;
8354        // `resumable { <handler> }`
8355        self.consume_contextual("resumable")?;
8356        let handler = self.parse_session_step_block()?;
8357        Ok(SessionStep {
8358            op: "interrupt".into(),
8359            message_type: signal.value,
8360            branches: vec![
8361                SessionBranch { label: "body".into(), steps: body, loc: loc.clone() },
8362                SessionBranch { label: "handler".into(), steps: handler, loc: loc.clone() },
8363            ],
8364            binder: binder.value,
8365            resumable: true,
8366            loc,
8367        })
8368    }
8369
8370    /// §Fase 41.b — Parse a choice step: `select { ask: [..], cancel: [..] }`
8371    /// (or `branch { … }`). Each `label: [steps]` arm is a nested sub-protocol.
8372    fn parse_session_choice(&mut self, op: &str, loc: Loc) -> Result<SessionStep, ParseError> {
8373        self.advance(); // consume `select` / `branch`
8374        self.consume(TokenType::LBrace)?;
8375        let mut branches = Vec::new();
8376        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8377            let label_tok = self.consume_any_ident_or_kw()?;
8378            self.consume(TokenType::Colon)?;
8379            let steps = self.parse_session_steps()?;
8380            branches.push(SessionBranch {
8381                label: label_tok.value,
8382                steps,
8383                loc: Loc { line: label_tok.line, column: label_tok.column },
8384            });
8385            if self.check(TokenType::Comma) {
8386                self.advance();
8387            }
8388        }
8389        self.consume(TokenType::RBrace)?;
8390        Ok(SessionStep { op: op.to_string(), branches, loc, ..Default::default() })
8391    }
8392
8393    /// Parse: `topology Name { nodes: [A, B, …]  edges: [A -> B : Session, …] }`.
8394    fn parse_topology(&mut self) -> Result<TopologyDefinition, ParseError> {
8395        let tok = self.consume(TokenType::Topology)?;
8396        let name = self.consume(TokenType::Identifier)?.value;
8397        let mut node = TopologyDefinition {
8398            name,
8399            nodes: Vec::new(),
8400            edges: Vec::new(),
8401            loc: Loc {
8402                line: tok.line,
8403                column: tok.column,
8404            },
8405            leading_trivia: Vec::new(),
8406            trailing_trivia: Vec::new(),
8407        };
8408        self.consume(TokenType::LBrace)?;
8409        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8410            let field_name = self.current().value.clone();
8411            self.advance();
8412            if !self.check(TokenType::Colon) {
8413                if self.check(TokenType::LBrace) {
8414                    self.skip_braced_block()?;
8415                }
8416                continue;
8417            }
8418            self.advance();
8419            match field_name.as_str() {
8420                "nodes" => node.nodes = self.parse_bracketed_identifiers()?,
8421                "edges" => node.edges = self.parse_topology_edges()?,
8422                _ => self.skip_value(),
8423            }
8424        }
8425        self.consume(TokenType::RBrace)?;
8426        Ok(node)
8427    }
8428
8429    fn parse_topology_edges(&mut self) -> Result<Vec<TopologyEdge>, ParseError> {
8430        self.consume(TokenType::LBracket)?;
8431        let mut edges = Vec::new();
8432        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
8433            edges.push(self.parse_topology_edge()?);
8434            if self.check(TokenType::Comma) {
8435                self.advance();
8436            }
8437        }
8438        self.consume(TokenType::RBracket)?;
8439        Ok(edges)
8440    }
8441
8442    fn parse_topology_edge(&mut self) -> Result<TopologyEdge, ParseError> {
8443        let src_tok = self.consume_any_ident_or_kw()?;
8444        self.consume(TokenType::Arrow)?;
8445        let tgt_tok = self.consume_any_ident_or_kw()?;
8446        self.consume(TokenType::Colon)?;
8447        let sess_tok = self.consume_any_ident_or_kw()?;
8448        Ok(TopologyEdge {
8449            source: src_tok.value,
8450            target: tgt_tok.value,
8451            session_ref: sess_tok.value,
8452            loc: Loc {
8453                line: src_tok.line,
8454                column: src_tok.column,
8455            },
8456        })
8457    }
8458
8459    // ── §λ-L-E Fase 5 — Cognitive immune system (paper_immune_v2.md) ────
8460
8461    /// Parse: `immune Name { watch, sensitivity, baseline, window, scope, tau, decay }`.
8462    fn parse_immune(&mut self) -> Result<ImmuneDefinition, ParseError> {
8463        let tok = self.consume(TokenType::Immune)?;
8464        let name = self.consume(TokenType::Identifier)?.value;
8465        let mut node = ImmuneDefinition {
8466            name,
8467            watch: Vec::new(),
8468            sensitivity: None,
8469            baseline: "learned".to_string(),
8470            window: 100,
8471            scope: String::new(),
8472            tau: String::new(),
8473            decay: "exponential".to_string(),
8474            loc: Loc {
8475                line: tok.line,
8476                column: tok.column,
8477            },
8478            leading_trivia: Vec::new(),
8479            trailing_trivia: Vec::new(),
8480        };
8481        self.consume(TokenType::LBrace)?;
8482        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8483            let field_name = self.current().value.clone();
8484            self.advance();
8485            if !self.check(TokenType::Colon) {
8486                if self.check(TokenType::LBrace) {
8487                    self.skip_braced_block()?;
8488                }
8489                continue;
8490            }
8491            self.advance();
8492            match field_name.as_str() {
8493                "watch" => node.watch = self.parse_bracketed_identifiers()?,
8494                "sensitivity" => node.sensitivity = self.parse_optional_float(),
8495                "baseline" => node.baseline = self.consume_any_ident_or_kw()?.value,
8496                "window" => {
8497                    if let Some(v) = self.parse_optional_int() {
8498                        node.window = v;
8499                    }
8500                }
8501                "scope" => {
8502                    let s_tok = self.consume_any_ident_or_kw()?;
8503                    let s = s_tok.value;
8504                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
8505                        return Err(ParseError {
8506                            message: format!(
8507                                "Invalid scope '{s}' in immune '{}' — \
8508                                 expected tenant | flow | global",
8509                                node.name
8510                            ),
8511                            line: s_tok.line,
8512                            column: s_tok.column,
8513                                                    ..Default::default()
8514                        });
8515                    }
8516                    node.scope = s;
8517                }
8518                "tau" => {
8519                    let t = self.current().clone();
8520                    match t.ttype {
8521                        TokenType::Duration | TokenType::StringLit => {
8522                            self.advance();
8523                            node.tau = t.value;
8524                        }
8525                        _ => node.tau = self.consume_any_ident_or_kw()?.value,
8526                    }
8527                }
8528                "decay" => {
8529                    let d_tok = self.consume_any_ident_or_kw()?;
8530                    let d = d_tok.value;
8531                    if !matches!(d.as_str(), "exponential" | "linear" | "none") {
8532                        return Err(ParseError {
8533                            message: format!(
8534                                "Invalid decay '{d}' in immune '{}' — \
8535                                 expected exponential | linear | none",
8536                                node.name
8537                            ),
8538                            line: d_tok.line,
8539                            column: d_tok.column,
8540                                                    ..Default::default()
8541                        });
8542                    }
8543                    node.decay = d;
8544                }
8545                _ => self.skip_value(),
8546            }
8547        }
8548        self.consume(TokenType::RBrace)?;
8549        Ok(node)
8550    }
8551
8552    /// Parse: `reflex Name { trigger, on_level, action, scope, sla }`.
8553    fn parse_reflex(&mut self) -> Result<ReflexDefinition, ParseError> {
8554        let tok = self.consume(TokenType::Reflex)?;
8555        let name = self.consume(TokenType::Identifier)?.value;
8556        let mut node = ReflexDefinition {
8557            name,
8558            trigger: String::new(),
8559            on_level: "doubt".to_string(),
8560            action: String::new(),
8561            scope: String::new(),
8562            sla: String::new(),
8563            loc: Loc {
8564                line: tok.line,
8565                column: tok.column,
8566            },
8567            leading_trivia: Vec::new(),
8568            trailing_trivia: Vec::new(),
8569        };
8570        self.consume(TokenType::LBrace)?;
8571        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8572            let field_name = self.current().value.clone();
8573            self.advance();
8574            if !self.check(TokenType::Colon) {
8575                if self.check(TokenType::LBrace) {
8576                    self.skip_braced_block()?;
8577                }
8578                continue;
8579            }
8580            self.advance();
8581            match field_name.as_str() {
8582                "trigger" => node.trigger = self.consume_any_ident_or_kw()?.value,
8583                "on_level" => {
8584                    let l_tok = self.consume_any_ident_or_kw()?;
8585                    let l = l_tok.value;
8586                    if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
8587                        return Err(ParseError {
8588                            message: format!(
8589                                "Invalid on_level '{l}' in reflex '{}' — \
8590                                 expected know | believe | speculate | doubt",
8591                                node.name
8592                            ),
8593                            line: l_tok.line,
8594                            column: l_tok.column,
8595                                                    ..Default::default()
8596                        });
8597                    }
8598                    node.on_level = l;
8599                }
8600                "action" => {
8601                    let a_tok = self.consume_any_ident_or_kw()?;
8602                    let a = a_tok.value;
8603                    if !matches!(
8604                        a.as_str(),
8605                        "drop"
8606                            | "revoke"
8607                            | "emit"
8608                            | "redact"
8609                            | "quarantine"
8610                            | "terminate"
8611                            | "alert"
8612                    ) {
8613                        return Err(ParseError {
8614                            message: format!(
8615                                "Invalid action '{a}' in reflex '{}' — \
8616                                 expected drop | revoke | emit | redact | \
8617                                 quarantine | terminate | alert",
8618                                node.name
8619                            ),
8620                            line: a_tok.line,
8621                            column: a_tok.column,
8622                                                    ..Default::default()
8623                        });
8624                    }
8625                    node.action = a;
8626                }
8627                "scope" => {
8628                    let s_tok = self.consume_any_ident_or_kw()?;
8629                    let s = s_tok.value;
8630                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
8631                        return Err(ParseError {
8632                            message: format!(
8633                                "Invalid scope '{s}' in reflex '{}' — \
8634                                 expected tenant | flow | global",
8635                                node.name
8636                            ),
8637                            line: s_tok.line,
8638                            column: s_tok.column,
8639                                                    ..Default::default()
8640                        });
8641                    }
8642                    node.scope = s;
8643                }
8644                "sla" => {
8645                    let t = self.current().clone();
8646                    match t.ttype {
8647                        TokenType::Duration | TokenType::StringLit => {
8648                            self.advance();
8649                            node.sla = t.value;
8650                        }
8651                        _ => node.sla = self.consume_any_ident_or_kw()?.value,
8652                    }
8653                }
8654                _ => self.skip_value(),
8655            }
8656        }
8657        self.consume(TokenType::RBrace)?;
8658        Ok(node)
8659    }
8660
8661    /// Parse: `heal Name { source, on_level, mode, scope, review_sla, shield, max_patches }`.
8662    fn parse_heal(&mut self) -> Result<HealDefinition, ParseError> {
8663        let tok = self.consume(TokenType::Heal)?;
8664        let name = self.consume(TokenType::Identifier)?.value;
8665        let mut node = HealDefinition {
8666            name,
8667            source: String::new(),
8668            on_level: "doubt".to_string(),
8669            mode: "human_in_loop".to_string(),
8670            scope: String::new(),
8671            review_sla: String::new(),
8672            shield_ref: String::new(),
8673            max_patches: 3,
8674            loc: Loc {
8675                line: tok.line,
8676                column: tok.column,
8677            },
8678            leading_trivia: Vec::new(),
8679            trailing_trivia: Vec::new(),
8680        };
8681        self.consume(TokenType::LBrace)?;
8682        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8683            let field_name = self.current().value.clone();
8684            self.advance();
8685            if !self.check(TokenType::Colon) {
8686                if self.check(TokenType::LBrace) {
8687                    self.skip_braced_block()?;
8688                }
8689                continue;
8690            }
8691            self.advance();
8692            match field_name.as_str() {
8693                "source" => node.source = self.consume_any_ident_or_kw()?.value,
8694                "on_level" => {
8695                    let l_tok = self.consume_any_ident_or_kw()?;
8696                    let l = l_tok.value;
8697                    if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
8698                        return Err(ParseError {
8699                            message: format!(
8700                                "Invalid on_level '{l}' in heal '{}' — \
8701                                 expected know | believe | speculate | doubt",
8702                                node.name
8703                            ),
8704                            line: l_tok.line,
8705                            column: l_tok.column,
8706                                                    ..Default::default()
8707                        });
8708                    }
8709                    node.on_level = l;
8710                }
8711                "mode" => {
8712                    let m_tok = self.consume_any_ident_or_kw()?;
8713                    let m = m_tok.value;
8714                    if !matches!(m.as_str(), "audit_only" | "human_in_loop" | "adversarial") {
8715                        return Err(ParseError {
8716                            message: format!(
8717                                "Invalid mode '{m}' in heal '{}' — \
8718                                 expected audit_only | human_in_loop | adversarial",
8719                                node.name
8720                            ),
8721                            line: m_tok.line,
8722                            column: m_tok.column,
8723                                                    ..Default::default()
8724                        });
8725                    }
8726                    node.mode = m;
8727                }
8728                "scope" => {
8729                    let s_tok = self.consume_any_ident_or_kw()?;
8730                    let s = s_tok.value;
8731                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
8732                        return Err(ParseError {
8733                            message: format!(
8734                                "Invalid scope '{s}' in heal '{}' — \
8735                                 expected tenant | flow | global",
8736                                node.name
8737                            ),
8738                            line: s_tok.line,
8739                            column: s_tok.column,
8740                                                    ..Default::default()
8741                        });
8742                    }
8743                    node.scope = s;
8744                }
8745                "review_sla" => {
8746                    let t = self.current().clone();
8747                    match t.ttype {
8748                        TokenType::Duration | TokenType::StringLit => {
8749                            self.advance();
8750                            node.review_sla = t.value;
8751                        }
8752                        _ => node.review_sla = self.consume_any_ident_or_kw()?.value,
8753                    }
8754                }
8755                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
8756                "max_patches" => {
8757                    if let Some(v) = self.parse_optional_int() {
8758                        node.max_patches = v;
8759                    }
8760                }
8761                _ => self.skip_value(),
8762            }
8763        }
8764        self.consume(TokenType::RBrace)?;
8765        Ok(node)
8766    }
8767
8768    // ── §λ-L-E Fase 9 — UI cognitiva (component / view) ────────────
8769
8770    /// Parse: `component Name { renders, via_shield, on_interact, render_hint }`.
8771    fn parse_component(&mut self) -> Result<ComponentDefinition, ParseError> {
8772        let tok = self.consume(TokenType::Component)?;
8773        let name = self.consume(TokenType::Identifier)?.value;
8774        let mut node = ComponentDefinition {
8775            name,
8776            renders: String::new(),
8777            via_shield: String::new(),
8778            on_interact: String::new(),
8779            render_hint: "custom".to_string(),
8780            loc: Loc {
8781                line: tok.line,
8782                column: tok.column,
8783            },
8784            leading_trivia: Vec::new(),
8785            trailing_trivia: Vec::new(),
8786        };
8787        self.consume(TokenType::LBrace)?;
8788        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8789            let field_name = self.current().value.clone();
8790            self.advance();
8791            if !self.check(TokenType::Colon) {
8792                if self.check(TokenType::LBrace) {
8793                    self.skip_braced_block()?;
8794                }
8795                continue;
8796            }
8797            self.advance();
8798            match field_name.as_str() {
8799                "renders" => node.renders = self.consume_any_ident_or_kw()?.value,
8800                "via_shield" => node.via_shield = self.consume_any_ident_or_kw()?.value,
8801                "on_interact" => node.on_interact = self.consume_any_ident_or_kw()?.value,
8802                "render_hint" => {
8803                    let h_tok = self.consume_any_ident_or_kw()?;
8804                    let h = h_tok.value;
8805                    if !matches!(h.as_str(), "card" | "list" | "form" | "chart" | "custom") {
8806                        return Err(ParseError {
8807                            message: format!(
8808                                "Invalid render_hint '{h}' in component '{}' — \
8809                                 expected card | list | form | chart | custom",
8810                                node.name
8811                            ),
8812                            line: h_tok.line,
8813                            column: h_tok.column,
8814                                                    ..Default::default()
8815                        });
8816                    }
8817                    node.render_hint = h;
8818                }
8819                _ => self.skip_value(),
8820            }
8821        }
8822        self.consume(TokenType::RBrace)?;
8823        Ok(node)
8824    }
8825
8826    /// Parse: `view Name { title, components: [...], route }`.
8827    fn parse_view(&mut self) -> Result<ViewDefinition, ParseError> {
8828        let tok = self.consume(TokenType::View)?;
8829        let name = self.consume(TokenType::Identifier)?.value;
8830        let mut node = ViewDefinition {
8831            name,
8832            title: String::new(),
8833            components: Vec::new(),
8834            route: String::new(),
8835            loc: Loc {
8836                line: tok.line,
8837                column: tok.column,
8838            },
8839            leading_trivia: Vec::new(),
8840            trailing_trivia: Vec::new(),
8841        };
8842        self.consume(TokenType::LBrace)?;
8843        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8844            let field_name = self.current().value.clone();
8845            self.advance();
8846            if !self.check(TokenType::Colon) {
8847                if self.check(TokenType::LBrace) {
8848                    self.skip_braced_block()?;
8849                }
8850                continue;
8851            }
8852            self.advance();
8853            match field_name.as_str() {
8854                "title" => node.title = self.consume(TokenType::StringLit)?.value,
8855                "components" => node.components = self.parse_bracketed_identifiers()?,
8856                "route" => node.route = self.consume(TokenType::StringLit)?.value,
8857                _ => self.skip_value(),
8858            }
8859        }
8860        self.consume(TokenType::RBrace)?;
8861        Ok(node)
8862    }
8863
8864    fn parse_axonendpoint(&mut self) -> Result<AxonEndpointDefinition, ParseError> {
8865        let tok = self.consume(TokenType::AxonEndpoint)?;
8866        let name = self.consume(TokenType::Identifier)?.value;
8867        let mut node = AxonEndpointDefinition {
8868            name,
8869            method: String::new(),
8870            path: String::new(),
8871            body_type: String::new(),
8872            execute_flow: String::new(),
8873            output_type: String::new(),
8874            shield_ref: String::new(),
8875            // §Fase 83.a — `cors:` reference; empty ≡ no cors declared
8876            // (D83.5: no CORS headers, ever — secure by default).
8877            cors_ref: String::new(),
8878            retries: None,
8879            timeout: String::new(),
8880            compliance: Vec::new(),
8881            // §Fase 30 — Defaults preserve backwards compat per D1.
8882            transport: "json".to_string(),
8883            keepalive: String::new(),
8884            // §Fase 31.b — Inference fields (parser-default state).
8885            // Both fields toggle/populate only when the source provides
8886            // an explicit `transport:` declaration (parser sets
8887            // `transport_explicit = true`) AND the type-checker walks
8888            // the program to compute `implicit_transport`.
8889            transport_explicit: false,
8890            implicit_transport: String::new(),
8891            // §Fase 32.g (D8) — auth scope; empty list ≡ no auth gate.
8892            requires_capabilities: Vec::new(),
8893            // §Fase 89.a — explicit authorization-coverage opt-out. Default
8894            // false; the §89.b rule requires coverage OR `public: true`.
8895            public: false,
8896            // §Fase 32.h — Replay-token binding (D9 plan-vivo).
8897            // Parser defaults: not explicit; effective value resolved
8898            // at deploy time using the method-default heuristic.
8899            replay_explicit: false,
8900            replay: false,
8901            // §Fase 33.z.k.b (v1.28.0) — Wire-format dialect default
8902            // empty; the runtime classifier resolves the default
8903            // dialect per the algebraic-effect predicate when the
8904            // source omits `transport: sse(<dialect>)`.
8905            transport_dialect: String::new(),
8906            // §Fase 33.z.k.1 (v1.27.1) — Algebraic-effect override.
8907            // Parser default false; populated by the type-checker's
8908            // compute_implicit_transports pass once the full program
8909            // is known (the predicate cross-references tool effects
8910            // declared anywhere in the program).
8911            has_algebraic_stream_effect: false,
8912            // §Fase 36.d (D2) — declared execution backend; empty ≡
8913            // not declared (the endpoint resolves down the Fase 36 D1
8914            // ladder). A non-empty value is validated against the
8915            // closed `AXONENDPOINT_BACKEND_VALUES` catalog below.
8916            backend: String::new(),
8917            // §Fase 37.y (D1) — Path-param names extracted from the
8918            // `path:` string AFTER the field is parsed. Initialized
8919            // empty; populated by `extract_path_param_names` after
8920            // the `path:` field is read in the loop below.
8921            path_params: Vec::new(),
8922            // §Fase 37.y (D2) — Inline `query: { name: Type, name: Type? }`
8923            // block. Initialized empty; populated by the `"query"` arm
8924            // in the field loop below. Closed catalog enforced at parse
8925            // time per `axonendpoint_is_valid_query_param_type`.
8926            query_params: Vec::new(),
8927            loc: Loc {
8928                line: tok.line,
8929                column: tok.column,
8930            },
8931            leading_trivia: Vec::new(),
8932            trailing_trivia: Vec::new(),
8933        };
8934        self.consume(TokenType::LBrace)?;
8935        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8936            let field_name = self.current().value.clone();
8937            self.advance();
8938            if self.check(TokenType::Colon) {
8939                self.advance();
8940                match field_name.as_str() {
8941                    "method" => {
8942                        // §Fase 32.b D3 — closed method enum
8943                        // `{GET, POST, PUT, DELETE, PATCH}`. Unknown
8944                        // values rejected at parse time with smart-
8945                        // suggest hint (Fase 28.e). HEAD/OPTIONS/etc.
8946                        // are runtime-managed and not adopter-
8947                        // declarable.
8948                        let value_tok = self.consume_any_ident_or_kw()?;
8949                        let value_upper = value_tok.value.to_uppercase();
8950                        if !axonendpoint_is_valid_method(&value_upper) {
8951                            let hint = crate::smart_suggest::suggest_for(
8952                                &value_upper,
8953                                AXONENDPOINT_METHOD_VALUES,
8954                            );
8955                            let base = format!(
8956                                "Invalid method '{}' in axonendpoint '{}'.",
8957                                value_tok.value, node.name
8958                            );
8959                            let message = if hint.is_empty() {
8960                                format!(
8961                                    "{base} expected GET | POST | PUT | DELETE | PATCH, found {}",
8962                                    value_tok.value
8963                                )
8964                            } else {
8965                                format!(
8966                                    "{base} {hint} (expected GET | POST | PUT | DELETE | PATCH, found {})",
8967                                    value_tok.value
8968                                )
8969                            };
8970                            return Err(ParseError {
8971                                message,
8972                                line: value_tok.line,
8973                                column: value_tok.column,
8974                                ..Default::default()
8975                            });
8976                        }
8977                        node.method = value_upper;
8978                    }
8979                    "path" => {
8980                        node.path = self.consume(TokenType::StringLit)?.value.clone();
8981                        // §Fase 37.y (D1) — extract `{name}` placeholders
8982                        // for the Request Binding Contract's path-param
8983                        // source. Duplicate `{name}` in the same path
8984                        // is rejected at parse time (HTTP route patterns
8985                        // structurally reject duplicates; surfacing the
8986                        // error here is friendlier than letting axum
8987                        // panic at registration).
8988                        match extract_path_param_names(&node.path) {
8989                            Ok(names) => node.path_params = names,
8990                            Err(dup) => {
8991                                let cur = self.current().clone();
8992                                return Err(ParseError {
8993                                    message: format!(
8994                                        "axonendpoint '{}' declares path '{}' \
8995                                         containing duplicate placeholder '{{{}}}'. \
8996                                         Each `{{name}}` in a `path:` must be \
8997                                         unique — the runtime cannot bind two \
8998                                         path segments to the same name (Fase 37.y D1).",
8999                                        node.name, node.path, dup,
9000                                    ),
9001                                    line: cur.line,
9002                                    column: cur.column,
9003                                    ..Default::default()
9004                                });
9005                            }
9006                        }
9007                    },
9008                    "body" => node.body_type = self.consume_any_ident_or_kw()?.value.clone(),
9009                    "query" => {
9010                        // §Fase 37.y (D2) — Inline query-parameter block.
9011                        // Grammar: `query: { name: Type [, name: Type?]* }`.
9012                        // Closed type catalog
9013                        // `AXONENDPOINT_QUERY_PARAM_TYPES = {Text, Int,
9014                        // Float, Bool, Uuid}`. Optional via `?` suffix
9015                        // reuses `TypeExpr.optional` semantics already in
9016                        // use for flow parameters + body type fields. A
9017                        // duplicate field name in the same block is a
9018                        // parse error (HTTP query strings DO allow
9019                        // multi-value but v1.38.5 binds the first value
9020                        // only — see plan vivo §7 forward-compat).
9021                        //
9022                        // §Fase 37.y (D2 robustness) — declaring `query:`
9023                        // twice on the same axonendpoint silently merged
9024                        // params pre-hardening. Now it's a parse error
9025                        // so an adopter typo / copy-paste mistake
9026                        // surfaces with line + column instead of
9027                        // producing an unexpectedly-augmented endpoint.
9028                        let lbrace_tok = self.consume(TokenType::LBrace)?;
9029                        let block_line = lbrace_tok.line;
9030                        if !node.query_params.is_empty() {
9031                            return Err(ParseError {
9032                                message: format!(
9033                                    "axonendpoint '{}' declares `query: {{ … }}` \
9034                                     more than once. The query-parameter block \
9035                                     is unique per endpoint; combine all params \
9036                                     into a single block (Fase 37.y D2).",
9037                                    node.name,
9038                                ),
9039                                line: lbrace_tok.line,
9040                                column: lbrace_tok.column,
9041                                ..Default::default()
9042                            });
9043                        }
9044                        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9045                            let name_tok = self.consume(TokenType::Identifier)?;
9046                            let field_name = name_tok.value.clone();
9047                            // Duplicate detection within the block.
9048                            if node
9049                                .query_params
9050                                .iter()
9051                                .any(|f| f.name == field_name)
9052                            {
9053                                return Err(ParseError {
9054                                    message: format!(
9055                                        "axonendpoint '{}' declares duplicate \
9056                                         query param '{}' inside `query: {{ … }}`. \
9057                                         Each name must appear at most once \
9058                                         (Fase 37.y D2).",
9059                                        node.name, field_name,
9060                                    ),
9061                                    line: name_tok.line,
9062                                    column: name_tok.column,
9063                                    ..Default::default()
9064                                });
9065                            }
9066                            self.consume(TokenType::Colon)?;
9067                            let type_expr = self.parse_type_expr()?;
9068                            // §Fase 37.y (D2 robustness) — reject generic
9069                            // type expressions on query params. The
9070                            // closed catalog is 5 primitives; container
9071                            // types (`Optional<T>`, `List<T>`, etc.)
9072                            // would mislead the adopter into thinking
9073                            // they bind multi-value query strings
9074                            // (deferred per plan vivo §7) or that
9075                            // `Optional<Text>` is the canonical way to
9076                            // declare an optional query (it's NOT —
9077                            // `Text?` is). Surface the canonical syntax
9078                            // verbatim so the fix is obvious.
9079                            if !type_expr.generic_param.is_empty() {
9080                                let canonical_hint = if type_expr.name == "Optional" {
9081                                    format!(
9082                                        " Use `{}?` (the `?` suffix) for an \
9083                                         optional query param instead of \
9084                                         `Optional<{}>`.",
9085                                        type_expr.generic_param,
9086                                        type_expr.generic_param,
9087                                    )
9088                                } else if type_expr.name == "List" {
9089                                    " Multi-value query params (e.g. `?tag=a&tag=b`) \
9090                                     are honest-deferred from v1.38.5; bind a \
9091                                     single-value `Text` query param and parse \
9092                                     the value inside the flow."
9093                                        .to_string()
9094                                } else {
9095                                    String::new()
9096                                };
9097                                return Err(ParseError {
9098                                    message: format!(
9099                                        "axonendpoint '{}' query param '{}' uses \
9100                                         a generic type `{}<{}>`. Query params \
9101                                         take a primitive type from the closed \
9102                                         catalog ({}); the `?` suffix marks \
9103                                         optional.{} (Fase 37.y D2).",
9104                                        node.name,
9105                                        field_name,
9106                                        type_expr.name,
9107                                        type_expr.generic_param,
9108                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | "),
9109                                        canonical_hint,
9110                                    ),
9111                                    line: type_expr.loc.line,
9112                                    column: type_expr.loc.column,
9113                                    ..Default::default()
9114                                });
9115                            }
9116                            // Validate against the closed catalog. A
9117                            // miss surfaces a Fase 28-style smart-suggest
9118                            // hint when within edit-distance 2.
9119                            if !axonendpoint_is_valid_query_param_type(&type_expr.name) {
9120                                // `smart_suggest::suggest_for` returns
9121                                // pre-formatted prose like
9122                                // "Did you mean `Text`?" or
9123                                // "Did you mean `Text` or `Int`?" (empty
9124                                // when no candidate within edit-distance
9125                                // 2). Concatenate without re-wrapping.
9126                                let hint = crate::smart_suggest::suggest_for(
9127                                    &type_expr.name,
9128                                    AXONENDPOINT_QUERY_PARAM_TYPES,
9129                                );
9130                                let hint_text = if hint.is_empty() {
9131                                    format!(
9132                                        " Expected one of: {}.",
9133                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | ")
9134                                    )
9135                                } else {
9136                                    format!(
9137                                        " {} Expected one of: {}.",
9138                                        hint,
9139                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | ")
9140                                    )
9141                                };
9142                                return Err(ParseError {
9143                                    message: format!(
9144                                        "axonendpoint '{}' query param '{}' has \
9145                                         unsupported type '{}'.{} (Fase 37.y D2).",
9146                                        node.name, field_name, type_expr.name,
9147                                        hint_text,
9148                                    ),
9149                                    line: type_expr.loc.line,
9150                                    column: type_expr.loc.column,
9151                                    ..Default::default()
9152                                });
9153                            }
9154                            node.query_params.push(TypeField {
9155                                name: field_name,
9156                                type_expr,
9157                                loc: Loc {
9158                                    line: name_tok.line,
9159                                    column: name_tok.column,
9160                                },
9161                            });
9162                            // Trailing comma is optional; the next loop
9163                            // iteration handles `}` cleanly. Accept both
9164                            // `name: Type, name: Type` AND `name: Type
9165                            // name: Type` (the existing parser style is
9166                            // forgiving about list separators).
9167                            if self.check(TokenType::Comma) {
9168                                self.advance();
9169                            }
9170                            let _ = block_line; // suppress unused warning
9171                        }
9172                        self.consume(TokenType::RBrace)?;
9173                    },
9174                    "execute" => node.execute_flow = self.consume_any_ident_or_kw()?.value.clone(),
9175                    "output" => {
9176                        // §Fase 38.x.f — promote axonendpoint `output:`
9177                        // parsing from a single token to the full
9178                        // generic-aware type expression (mirroring
9179                        // `parse_step` for FlowStep::Step which already
9180                        // uses `parse_output_type_string`).
9181                        //
9182                        // Pre-38.x.f: `output: List<Item>` captured only
9183                        // `"List"`, dropping `<Item>` (next tokens were
9184                        // either left unconsumed or absorbed by the
9185                        // following field). v1.39.0's narrow cardinality
9186                        // gate happened to fire correctly for `output: T`
9187                        // + retrieve-tail because the singular-detection
9188                        // path used `!starts_with("List<")` — but the
9189                        // SYMMETRIC `output: List<T>` + singular-tail
9190                        // case (38.x.f D3) needs the FULL `List<T>`
9191                        // shape captured; without it the gate sees
9192                        // `"List"` and misclassifies as Singular.
9193                        node.output_type = self.parse_output_type_string()?;
9194                    }
9195                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
9196                    // §Fase 83.a — the `cors: <Name>` reference.
9197                    "cors" => node.cors_ref = self.consume_any_ident_or_kw()?.value.clone(),
9198                    "retries" => node.retries = self.parse_optional_int(),
9199                    "timeout" => {
9200                        let t = self.current().clone();
9201                        self.advance();
9202                        node.timeout = t.value.clone();
9203                    }
9204                    "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
9205                    "replay" => {
9206                        // §Fase 32.h (D9 plan-vivo) — Replay-token binding.
9207                        // Boolean `replay: true | false`. Default (when
9208                        // omitted) is method-derived at deploy-time:
9209                        // POST/PUT → true, GET/DELETE → false. Explicit
9210                        // declaration sets `replay_explicit = true` so
9211                        // the runtime knows NOT to override.
9212                        let value_tok = self.consume(TokenType::Bool)?;
9213                        node.replay = value_tok.value.eq_ignore_ascii_case("true");
9214                        node.replay_explicit = true;
9215                    }
9216                    // §Fase 89.a — `public: true | false`, the explicit
9217                    // authorization-coverage opt-out (doctrine
9218                    // `every_boundary_is_guarded`). Mirrors `replay:`'s bool
9219                    // parse. Default false; the §89.b rule (`axon-T890`)
9220                    // requires a covering discipline OR `public: true`.
9221                    "public" => {
9222                        let value_tok = self.consume(TokenType::Bool)?;
9223                        node.public = value_tok.value.eq_ignore_ascii_case("true");
9224                    }
9225                    "requires" => {
9226                        // §Fase 32.g (D8) — Auth scope per axonendpoint.
9227                        // Closed slug grammar
9228                        // `^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$` enforced
9229                        // at parse time with smart-suggest-style hint.
9230                        // Empty list means "no auth gate" (D9 backwards-
9231                        // compat). Cross-stack with Python parser.
9232                        let bracket_tok = self.current().clone();
9233                        let items = self.parse_bracketed_dot_identifiers()?;
9234                        for slug in &items {
9235                            if !is_valid_capability_slug(slug) {
9236                                return Err(ParseError {
9237                                    message: format!(
9238                                        "Invalid capability slug '{slug}' in axonendpoint '{}' \
9239                                         `requires:`. Capability slugs must match \
9240                                         ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
9241                                         lowercase identifiers starting with a letter. Examples: \
9242                                         `admin`, `legal.read`, `hipaa.phi.read`.",
9243                                        node.name
9244                                    ),
9245                                    line: bracket_tok.line,
9246                                    column: bracket_tok.column,
9247                                    ..Default::default()
9248                                });
9249                            }
9250                        }
9251                        node.requires_capabilities = items;
9252                    }
9253                    // §Fase 30.b — HTTP transport enum (D2 closed) + keepalive (D6 closed).
9254                    // Mirrors `axon/compiler/parser.py` `_parse_axonendpoint`.
9255                    // Drift-gate corpus verifies byte-identical parse cross-stack.
9256                    "transport" => {
9257                        let value_tok = self.consume_any_ident_or_kw()?;
9258                        let value = &value_tok.value;
9259                        if !axonendpoint_is_valid_transport(value) {
9260                            let hint = crate::smart_suggest::suggest_for(
9261                                value,
9262                                AXONENDPOINT_TRANSPORT_VALUES,
9263                            );
9264                            let base = format!(
9265                                "Invalid transport '{}' in axonendpoint '{}'.",
9266                                value, node.name
9267                            );
9268                            let message = if hint.is_empty() {
9269                                format!("{base} expected json | sse | ndjson, found {value}")
9270                            } else {
9271                                format!(
9272                                    "{base} {hint} (expected json | sse | ndjson, found {value})"
9273                                )
9274                            };
9275                            return Err(ParseError {
9276                                message,
9277                                line: value_tok.line,
9278                                column: value_tok.column,
9279                                ..Default::default()
9280                            });
9281                        }
9282                        node.transport = value.clone();
9283                        // §Fase 31.b D1 — mark the field as explicitly
9284                        // declared so the type-checker's implicit-transport
9285                        // inference knows NOT to override this value with
9286                        // the produces_stream-driven inference.
9287                        node.transport_explicit = true;
9288                        // §Fase 33.z.k.b (v1.28.0) — Optional dialect
9289                        // parametrization: `transport: sse(<dialect>)`.
9290                        // Only valid when the base value is `sse`
9291                        // (json + ndjson dialects are the dialects
9292                        // themselves; `json(<x>)` / `ndjson(<x>)`
9293                        // would be parse errors caught below).
9294                        if self.check(TokenType::LParen) {
9295                            if value != "sse" {
9296                                let tok = self.current().clone();
9297                                return Err(ParseError {
9298                                    message: format!(
9299                                        "Dialect parametrization \
9300                                         `transport: {value}(<dialect>)` is \
9301                                         only valid for `sse`; got \
9302                                         `{value}` in axonendpoint '{}'.",
9303                                        node.name
9304                                    ),
9305                                    line: tok.line,
9306                                    column: tok.column,
9307                                    ..Default::default()
9308                                });
9309                            }
9310                            self.advance(); // consume LParen
9311                            let dialect_tok = self.consume_any_ident_or_kw()?;
9312                            let dialect = dialect_tok.value.clone();
9313                            if !AXONENDPOINT_TRANSPORT_DIALECTS
9314                                .iter()
9315                                .any(|&d| d == dialect)
9316                            {
9317                                let hint = crate::smart_suggest::suggest_for(
9318                                    &dialect,
9319                                    AXONENDPOINT_TRANSPORT_DIALECTS,
9320                                );
9321                                let base = format!(
9322                                    "Invalid SSE dialect '{dialect}' in axonendpoint '{}'.",
9323                                    node.name
9324                                );
9325                                let message = if hint.is_empty() {
9326                                    format!(
9327                                        "{base} expected axon | openai | kimi | glm | anthropic, found {dialect}"
9328                                    )
9329                                } else {
9330                                    format!(
9331                                        "{base} {hint} (expected axon | openai | kimi | glm | anthropic, found {dialect})"
9332                                    )
9333                                };
9334                                return Err(ParseError {
9335                                    message,
9336                                    line: dialect_tok.line,
9337                                    column: dialect_tok.column,
9338                                    ..Default::default()
9339                                });
9340                            }
9341                            // Closing RParen.
9342                            let rparen_tok = self.current().clone();
9343                            if !self.check(TokenType::RParen) {
9344                                return Err(ParseError {
9345                                    message: format!(
9346                                        "Expected `)` after dialect name \
9347                                         in axonendpoint '{}' \
9348                                         (transport: sse(<dialect>) grammar).",
9349                                        node.name
9350                                    ),
9351                                    line: rparen_tok.line,
9352                                    column: rparen_tok.column,
9353                                    ..Default::default()
9354                                });
9355                            }
9356                            self.advance(); // consume RParen
9357                            node.transport_dialect = dialect;
9358                        }
9359                    }
9360                    "keepalive" => {
9361                        // Accepts either a DURATION token (e.g. `15s`) or
9362                        // an ident-like token. Validation against the
9363                        // closed enum {5s, 15s, 30s, 60s} happens after.
9364                        let value_tok = self.current().clone();
9365                        self.advance();
9366                        let value = &value_tok.value;
9367                        if !axonendpoint_is_valid_keepalive(value) {
9368                            let hint = crate::smart_suggest::suggest_for(
9369                                value,
9370                                AXONENDPOINT_KEEPALIVE_VALUES,
9371                            );
9372                            let base = format!(
9373                                "Invalid keepalive '{}' in axonendpoint '{}'.",
9374                                value, node.name
9375                            );
9376                            let message = if hint.is_empty() {
9377                                format!("{base} expected 5s | 15s | 30s | 60s, found {value}")
9378                            } else {
9379                                format!(
9380                                    "{base} {hint} (expected 5s | 15s | 30s | 60s, found {value})"
9381                                )
9382                            };
9383                            return Err(ParseError {
9384                                message,
9385                                line: value_tok.line,
9386                                column: value_tok.column,
9387                                ..Default::default()
9388                            });
9389                        }
9390                        node.keepalive = value.clone();
9391                    }
9392                    "backend" => {
9393                        // §Fase 36.d (D2) — declared execution backend.
9394                        // Closed catalog `CANONICAL_PROVIDERS ∪ {auto,
9395                        // stub}`; an unknown name is a parse error with
9396                        // a smart-suggest hint (the same discipline as
9397                        // `method`/`transport`/`keepalive`). The
9398                        // type-checker re-validates defensively for
9399                        // ASTs built outside the parser (LSP, tests).
9400                        let value_tok = self.consume_any_ident_or_kw()?;
9401                        let value = &value_tok.value;
9402                        if !axonendpoint_is_valid_backend(value) {
9403                            let hint = crate::smart_suggest::suggest_for(
9404                                value,
9405                                AXONENDPOINT_BACKEND_VALUES,
9406                            );
9407                            let expected = AXONENDPOINT_BACKEND_VALUES.join(" | ");
9408                            let base = format!(
9409                                "Invalid backend '{}' in axonendpoint '{}'.",
9410                                value, node.name
9411                            );
9412                            let message = if hint.is_empty() {
9413                                format!("{base} expected {expected}, found {value}")
9414                            } else {
9415                                format!(
9416                                    "{base} {hint} (expected {expected}, found {value})"
9417                                )
9418                            };
9419                            return Err(ParseError {
9420                                message,
9421                                line: value_tok.line,
9422                                column: value_tok.column,
9423                                ..Default::default()
9424                            });
9425                        }
9426                        node.backend = value.clone();
9427                    }
9428                    _ => self.skip_value(),
9429                }
9430            } else if self.check(TokenType::LBrace) {
9431                self.skip_braced_block()?;
9432            }
9433        }
9434        self.consume(TokenType::RBrace)?;
9435        Ok(node)
9436    }
9437
9438    // ── Numeric helpers for Tier 2 field parsing ────────────────────
9439
9440    fn parse_optional_int(&mut self) -> Option<i64> {
9441        let tok = self.current().clone();
9442        match tok.ttype {
9443            TokenType::Integer => {
9444                self.advance();
9445                tok.value.parse::<i64>().ok()
9446            }
9447            _ => {
9448                self.advance();
9449                None
9450            }
9451        }
9452    }
9453
9454    fn parse_optional_float(&mut self) -> Option<f64> {
9455        let tok = self.current().clone();
9456        match tok.ttype {
9457            TokenType::Float | TokenType::Integer => {
9458                self.advance();
9459                tok.value.parse::<f64>().ok()
9460            }
9461            _ => {
9462                self.advance();
9463                None
9464            }
9465        }
9466    }
9467
9468    // ── LAMBDA DATA (ΛD) ──────────────────────────────────────────
9469
9470    fn parse_lambda_data(&mut self) -> Result<LambdaDataDefinition, ParseError> {
9471        let tok = self.consume(TokenType::Lambda)?;
9472        let name = self.consume(TokenType::Identifier)?;
9473        self.consume(TokenType::LBrace)?;
9474
9475        let mut node = LambdaDataDefinition {
9476            name: name.value.clone(),
9477            ontology: String::new(),
9478            certainty: 1.0,
9479            temporal_frame_start: String::new(),
9480            temporal_frame_end: String::new(),
9481            provenance: String::new(),
9482            derivation: String::new(),
9483            loc: Loc {
9484                line: tok.line,
9485                column: tok.column,
9486            },
9487            leading_trivia: Vec::new(),
9488            trailing_trivia: Vec::new(),
9489        };
9490
9491        while !self.check(TokenType::RBrace) {
9492            let field = self.current().clone();
9493            match field.ttype {
9494                TokenType::Ontology => {
9495                    self.advance();
9496                    self.consume(TokenType::Colon)?;
9497                    node.ontology = self.consume(TokenType::StringLit)?.value.clone();
9498                }
9499                TokenType::Certainty => {
9500                    self.advance();
9501                    self.consume(TokenType::Colon)?;
9502                    let val = self.current().clone();
9503                    match val.ttype {
9504                        TokenType::Float => {
9505                            self.advance();
9506                            node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
9507                        }
9508                        TokenType::Integer => {
9509                            self.advance();
9510                            node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
9511                        }
9512                        _ => {
9513                            return Err(ParseError {
9514                                message: format!(
9515                                    "Expected number for certainty, got '{}'",
9516                                    val.value
9517                                ),
9518                                line: val.line,
9519                                column: val.column,
9520                                                            ..Default::default()
9521                            });
9522                        }
9523                    }
9524                }
9525                TokenType::TemporalFrame => {
9526                    self.advance();
9527                    self.consume(TokenType::Colon)?;
9528                    node.temporal_frame_start = self.consume(TokenType::StringLit)?.value.clone();
9529                    // Optional second string for end frame
9530                    if self.check(TokenType::StringLit) {
9531                        node.temporal_frame_end = self.consume(TokenType::StringLit)?.value.clone();
9532                    }
9533                }
9534                TokenType::Provenance => {
9535                    self.advance();
9536                    self.consume(TokenType::Colon)?;
9537                    node.provenance = self.consume(TokenType::StringLit)?.value.clone();
9538                }
9539                TokenType::Derivation => {
9540                    self.advance();
9541                    self.consume(TokenType::Colon)?;
9542                    let d = self.current().clone();
9543                    self.advance();
9544                    node.derivation = d.value.clone();
9545                }
9546                _ => {
9547                    // Skip unknown fields gracefully
9548                    self.advance();
9549                    if self.check(TokenType::Colon) {
9550                        self.advance();
9551                        self.skip_value();
9552                    }
9553                }
9554            }
9555        }
9556
9557        self.consume(TokenType::RBrace)?;
9558        Ok(node)
9559    }
9560
9561    fn parse_lambda_data_apply(&mut self) -> Result<LambdaDataApplyNode, ParseError> {
9562        let tok = self.consume(TokenType::Lambda)?;
9563        let lambda_name = self.consume(TokenType::Identifier)?;
9564
9565        // Expect "on" keyword (parsed as identifier since it's not reserved)
9566        let on_tok = self.current().clone();
9567        self.advance();
9568        if on_tok.value != "on" {
9569            return Err(ParseError {
9570                message: format!(
9571                    "Expected 'on' after lambda data name in flow step, got '{}'",
9572                    on_tok.value
9573                ),
9574                line: on_tok.line,
9575                column: on_tok.column,
9576                            ..Default::default()
9577            });
9578        }
9579
9580        let target = self.current().clone();
9581        self.advance();
9582
9583        let mut output_type = String::new();
9584        if self.check(TokenType::Arrow) {
9585            self.advance();
9586            output_type = self.consume(TokenType::Identifier)?.value.clone();
9587        }
9588
9589        Ok(LambdaDataApplyNode {
9590            lambda_data_name: lambda_name.value.clone(),
9591            target: target.value.clone(),
9592            output_type,
9593            loc: Loc {
9594                line: tok.line,
9595                column: tok.column,
9596            },
9597        })
9598    }
9599
9600    // ── GENERIC (Tier 2+) ────────────────────────────────────────
9601
9602    fn parse_generic_declaration(&mut self) -> Result<Declaration, ParseError> {
9603        let kw_tok = self.current().clone();
9604        self.advance(); // consume keyword
9605
9606        // Try to consume a name (identifier or keyword-as-name)
9607        let name = if self.current().ttype == TokenType::Identifier {
9608            let n = self.current().value.clone();
9609            self.advance();
9610            n
9611        } else if !self.check(TokenType::LBrace)
9612            && !self.check(TokenType::LParen)
9613            && !self.check(TokenType::Eof)
9614            && self
9615                .current()
9616                .value
9617                .chars()
9618                .all(|c| c.is_alphanumeric() || c == '_')
9619        {
9620            let n = self.current().value.clone();
9621            self.advance();
9622            n
9623        } else {
9624            String::new()
9625        };
9626
9627        // Skip optional parens: (...)
9628        if self.check(TokenType::LParen) {
9629            self.advance();
9630            let mut depth = 1u32;
9631            while depth > 0 && !self.check(TokenType::Eof) {
9632                if self.check(TokenType::LParen) {
9633                    depth += 1;
9634                } else if self.check(TokenType::RParen) {
9635                    depth -= 1;
9636                }
9637                self.advance();
9638            }
9639        }
9640
9641        // Skip tokens until LBrace or next declaration
9642        while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
9643            if self.check(TokenType::Eof) {
9644                break;
9645            }
9646            self.advance();
9647        }
9648
9649        // Skip braced block if present
9650        if self.check(TokenType::LBrace) {
9651            self.skip_braced_block()?;
9652        }
9653
9654        Ok(Declaration::Generic(GenericDeclaration {
9655            keyword: kw_tok.value,
9656            name,
9657            loc: Loc {
9658                line: kw_tok.line,
9659                column: kw_tok.column,
9660            },
9661            leading_trivia: Vec::new(),
9662            trailing_trivia: Vec::new(),
9663        }))
9664    }
9665
9666    // ──────────────────────────────────────────────────────────────────
9667    //  §λ-L-E Fase 13 — Mobile Typed Channels parsers
9668    //  (paper_mobile_channels.md §3 + plan/fase_13)
9669    //  Direct port of axon/compiler/parser.py:_parse_channel/emit/publish/discover.
9670    // ──────────────────────────────────────────────────────────────────
9671
9672    /// Parse: `channel Name { message, qos, lifetime, persistence, shield }`.
9673    fn parse_channel(&mut self) -> Result<ChannelDefinition, ParseError> {
9674        let tok = self.consume(TokenType::Channel)?;
9675        let name = self.consume(TokenType::Identifier)?.value;
9676        let mut node = ChannelDefinition {
9677            name: name.clone(),
9678            message: String::new(),
9679            qos: "at_least_once".to_string(),
9680            lifetime: "affine".to_string(),
9681            persistence: "ephemeral".to_string(),
9682            shield_ref: String::new(),
9683            loc: Loc {
9684                line: tok.line,
9685                column: tok.column,
9686            },
9687            leading_trivia: Vec::new(),
9688            trailing_trivia: Vec::new(),
9689        };
9690        self.consume(TokenType::LBrace)?;
9691        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9692            let field_tok = self.current().clone();
9693            let field_name = field_tok.value.clone();
9694            self.advance();
9695            if !self.check(TokenType::Colon) {
9696                if self.check(TokenType::LBrace) {
9697                    self.skip_braced_block()?;
9698                }
9699                continue;
9700            }
9701            self.advance();
9702            match field_name.as_str() {
9703                "message" => node.message = self.parse_channel_message_type()?,
9704                "qos" => {
9705                    let q_tok = self.consume_any_ident_or_kw()?;
9706                    if !matches!(
9707                        q_tok.value.as_str(),
9708                        "at_most_once" | "at_least_once" | "exactly_once" | "broadcast" | "queue"
9709                    ) {
9710                        return Err(ParseError {
9711                            message: format!(
9712                                "Invalid qos '{}' in channel '{}' — \
9713                                 expected at_most_once | at_least_once | \
9714                                 exactly_once | broadcast | queue",
9715                                q_tok.value, name
9716                            ),
9717                            line: q_tok.line,
9718                            column: q_tok.column,
9719                                                    ..Default::default()
9720                        });
9721                    }
9722                    node.qos = q_tok.value;
9723                }
9724                "lifetime" => {
9725                    let lt_tok = self.consume_any_ident_or_kw()?;
9726                    if !matches!(lt_tok.value.as_str(), "linear" | "affine" | "persistent") {
9727                        return Err(ParseError {
9728                            message: format!(
9729                                "Invalid lifetime '{}' in channel '{}' — \
9730                                 expected linear | affine | persistent",
9731                                lt_tok.value, name
9732                            ),
9733                            line: lt_tok.line,
9734                            column: lt_tok.column,
9735                                                    ..Default::default()
9736                        });
9737                    }
9738                    node.lifetime = lt_tok.value;
9739                }
9740                "persistence" => {
9741                    let p_tok = self.consume_any_ident_or_kw()?;
9742                    if !matches!(p_tok.value.as_str(), "ephemeral" | "persistent_axonstore") {
9743                        return Err(ParseError {
9744                            message: format!(
9745                                "Invalid persistence '{}' in channel '{}' — \
9746                                 expected ephemeral | persistent_axonstore",
9747                                p_tok.value, name
9748                            ),
9749                            line: p_tok.line,
9750                            column: p_tok.column,
9751                                                    ..Default::default()
9752                        });
9753                    }
9754                    node.persistence = p_tok.value;
9755                }
9756                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
9757                _ => self.skip_value(),
9758            }
9759        }
9760        self.consume(TokenType::RBrace)?;
9761        Ok(node)
9762    }
9763
9764    /// Parse a `message:` value, supporting nested `Channel<…>`
9765    /// (second-order session types — paper §3.3).
9766    fn parse_channel_message_type(&mut self) -> Result<String, ParseError> {
9767        let head = self.consume(TokenType::Identifier)?;
9768        let mut spelling = head.value;
9769        if self.check(TokenType::Lt) {
9770            self.advance();
9771            let inner = self.parse_channel_message_type()?;
9772            self.consume(TokenType::Gt)?;
9773            spelling = format!("{}<{}>", spelling, inner);
9774        }
9775        Ok(spelling)
9776    }
9777
9778    /// Parse: `emit ChannelName(value_ref)` — Chan-Output / Chan-Mobility.
9779    ///
9780    /// `value_ref` accepts a bare identifier (variable / channel name for
9781    /// mobility) or a dotted path (`Step.output.field`) referencing a prior
9782    /// step result (Fase 13.i — runtime resolves via ContextManager).
9783    fn parse_emit_step(&mut self) -> Result<FlowStep, ParseError> {
9784        let tok = self.consume(TokenType::Emit)?;
9785        let channel = self.consume(TokenType::Identifier)?.value;
9786        self.consume(TokenType::LParen)?;
9787        let value = self.parse_emit_value_ref()?;
9788        self.consume(TokenType::RParen)?;
9789        Ok(FlowStep::Emit(EmitStatement {
9790            channel_ref: channel,
9791            value_ref: value,
9792            loc: Loc {
9793                line: tok.line,
9794                column: tok.column,
9795            },
9796        }))
9797    }
9798
9799    /// §Fase 92.b — parse `mint <Credential> as <binding>`. The credential
9800    /// reference must resolve to a declared `credential` (`axon-T895`,
9801    /// type-checker); the binding is a fresh flow-scoped name receiving the
9802    /// raw bearer string. Both tokens are required — a `mint` with no
9803    /// binding would mint authority into the void.
9804    fn parse_mint_step(&mut self) -> Result<FlowStep, ParseError> {
9805        let tok = self.consume(TokenType::Mint)?;
9806        let credential_ref = self.consume(TokenType::Identifier)?.value;
9807        self.consume(TokenType::As)?;
9808        let binding = self.consume(TokenType::Identifier)?.value;
9809        Ok(FlowStep::Mint(MintStep {
9810            credential_ref,
9811            binding,
9812            loc: Loc {
9813                line: tok.line,
9814                column: tok.column,
9815            },
9816        }))
9817    }
9818
9819    /// §Fase 94.b — parse `rotate <SecretsStore> [where "<filter>"] with
9820    /// <Tool> as <binding>` (doctrine `rotation_without_revelation`).
9821    ///
9822    /// All three anchors are grammar, not convention: the store names WHAT
9823    /// may rotate (a `backend: secrets` class view — `axon-T898` in the
9824    /// type-checker), the tool names WHO performs the exchange
9825    /// (`axon-T899`), and the binding receives the metadata-only summary —
9826    /// a `rotate` without a binding would renew authority with no
9827    /// observable outcome, so `as` is REQUIRED (the `mint` posture). The
9828    /// `where` filter is optional (§67 string grammar, proven against the
9829    /// synthesized metadata schema); omitting it rotates the WHOLE class —
9830    /// the deliberate post-breach bulk shape. `with` is a soft keyword
9831    /// (not a lexer token): reserving it globally would break every
9832    /// adopter identifier named `with`.
9833    fn parse_rotate_step(&mut self) -> Result<FlowStep, ParseError> {
9834        let tok = self.consume(TokenType::Rotate)?;
9835        let store_ref = self.consume(TokenType::Identifier)?.value;
9836        let mut where_expr = String::new();
9837        if self.check(TokenType::Where) {
9838            self.advance();
9839            where_expr = self.consume(TokenType::StringLit)?.value.clone();
9840        }
9841        let with_tok = self.current().clone();
9842        if with_tok.value != "with" {
9843            return Err(ParseError {
9844                message: format!(
9845                    "Expected `with <Tool>` after `rotate {store_ref}{}`, found '{}'. \
9846                     A rotation names the tool that performs the renewal exchange: \
9847                     `rotate {store_ref} [where \"<filter>\"] with <Tool> as <binding>`.",
9848                    if where_expr.is_empty() { "" } else { " where …" },
9849                    with_tok.value
9850                ),
9851                line: with_tok.line,
9852                column: with_tok.column,
9853                ..Default::default()
9854            });
9855        }
9856        self.advance();
9857        let tool_ref = self.consume(TokenType::Identifier)?.value;
9858        self.consume(TokenType::As)?;
9859        let binding = self.consume(TokenType::Identifier)?.value;
9860        Ok(FlowStep::Rotate(RotateStep {
9861            store_ref,
9862            where_expr,
9863            tool_ref,
9864            binding,
9865            loc: Loc {
9866                line: tok.line,
9867                column: tok.column,
9868            },
9869        }))
9870    }
9871
9872    /// Parse: `IDENTIFIER ('.' (IDENTIFIER | keyword))*` → dot-joined string
9873    /// (Fase 13.i).
9874    ///
9875    /// Mirrors the Python `_parse_emit_value_ref` helper exactly so the IR
9876    /// JSON for `emit Hello(Build.output)` is byte-identical between the
9877    /// two reference implementations.
9878    ///
9879    /// The HEAD must be a real ``Identifier``. Subsequent segments after a
9880    /// `.` may be identifiers OR keywords — common field names like
9881    /// ``output``, ``result``, ``message``, ``state``, etc. are reserved
9882    /// words in Axon but adopters must be able to write them as
9883    /// dotted-access segments. The accepting predicate:
9884    ///   - the lexer carried a non-empty `value` (every Word-like token does)
9885    ///   - the value's first byte is a letter or underscore (filters out
9886    ///     punctuation tokens such as ',', '{', etc.)
9887    fn parse_emit_value_ref(&mut self) -> Result<String, ParseError> {
9888        let head = self.consume(TokenType::Identifier)?.value;
9889        let mut parts = vec![head];
9890        while self.check(TokenType::Dot) {
9891            self.advance(); // consume '.'
9892            let next_tok = self.current().clone();
9893            let valid = !next_tok.value.is_empty()
9894                && next_tok.value.as_bytes()[0].is_ascii_alphabetic()
9895                || next_tok.value.starts_with('_');
9896            if !valid {
9897                return Err(ParseError {
9898                    message: format!(
9899                        "Expected identifier or keyword after '.' in dotted \
9900                         access, found {:?}",
9901                        next_tok.value
9902                    ),
9903                    line: next_tok.line,
9904                    column: next_tok.column,
9905                                    ..Default::default()
9906                });
9907            }
9908            self.advance();
9909            parts.push(next_tok.value);
9910        }
9911        Ok(parts.join("."))
9912    }
9913
9914    /// Parse: `publish ChannelName within ShieldName` — Publish-Ext (D8).
9915    fn parse_publish_step(&mut self) -> Result<FlowStep, ParseError> {
9916        let tok = self.consume(TokenType::Publish)?;
9917        let channel = self.consume(TokenType::Identifier)?.value;
9918        self.consume(TokenType::Within)?;
9919        let shield = self.consume(TokenType::Identifier)?.value;
9920        Ok(FlowStep::Publish(PublishStatement {
9921            channel_ref: channel,
9922            shield_ref: shield,
9923            loc: Loc {
9924                line: tok.line,
9925                column: tok.column,
9926            },
9927        }))
9928    }
9929
9930    /// Parse: `discover ChannelName as alias` — dual of publish.
9931    fn parse_discover_step(&mut self) -> Result<FlowStep, ParseError> {
9932        let tok = self.consume(TokenType::Discover)?;
9933        let cap = self.consume(TokenType::Identifier)?.value;
9934        self.consume(TokenType::As)?;
9935        let alias = self.consume(TokenType::Identifier)?.value;
9936        Ok(FlowStep::Discover(DiscoverStatement {
9937            capability_ref: cap,
9938            alias,
9939            loc: Loc {
9940                line: tok.line,
9941                column: tok.column,
9942            },
9943        }))
9944    }
9945}
9946
9947// ── §λ-L-E Fase 13 — Mobile Typed Channels parser tests ─────────────────────
9948
9949#[cfg(test)]
9950mod fase13_parser_tests {
9951    use super::*;
9952    use crate::lexer::Lexer;
9953
9954    fn parse(src: &str) -> Result<Program, ParseError> {
9955        let tokens = Lexer::new(src, "<test>").tokenize().expect("lex");
9956        Parser::new(tokens).parse()
9957    }
9958
9959    #[test]
9960    fn channel_full_parses() {
9961        let src = r#"channel C { message: Order qos: at_least_once lifetime: affine persistence: ephemeral shield: Gate }"#;
9962        let prog = parse(src).expect("parse");
9963        match &prog.declarations[0] {
9964            Declaration::Channel(c) => {
9965                assert_eq!(c.name, "C");
9966                assert_eq!(c.message, "Order");
9967                assert_eq!(c.qos, "at_least_once");
9968                assert_eq!(c.lifetime, "affine");
9969                assert_eq!(c.persistence, "ephemeral");
9970                assert_eq!(c.shield_ref, "Gate");
9971            }
9972            _ => panic!("expected ChannelDefinition"),
9973        }
9974    }
9975
9976    #[test]
9977    fn channel_defaults_match_paper_d1() {
9978        let prog = parse("channel C { message: Order }").expect("parse");
9979        if let Declaration::Channel(c) = &prog.declarations[0] {
9980            assert_eq!(c.qos, "at_least_once"); // default
9981            assert_eq!(c.lifetime, "affine"); // D1 default
9982            assert_eq!(c.persistence, "ephemeral");
9983            assert_eq!(c.shield_ref, "");
9984        } else {
9985            panic!("expected ChannelDefinition");
9986        }
9987    }
9988
9989    #[test]
9990    fn channel_second_order_message_type_parses() {
9991        let prog = parse("channel C { message: Channel<Order> }").expect("parse");
9992        if let Declaration::Channel(c) = &prog.declarations[0] {
9993            assert_eq!(c.message, "Channel<Order>");
9994        } else {
9995            panic!("expected ChannelDefinition");
9996        }
9997    }
9998
9999    #[test]
10000    fn channel_nested_channel_message_type_parses() {
10001        let prog = parse("channel C { message: Channel<Channel<Order>> }").expect("parse");
10002        if let Declaration::Channel(c) = &prog.declarations[0] {
10003            assert_eq!(c.message, "Channel<Channel<Order>>");
10004        } else {
10005            panic!("expected ChannelDefinition");
10006        }
10007    }
10008
10009    #[test]
10010    fn channel_invalid_qos_rejected() {
10011        let err = parse("channel C { message: T qos: bogus }").unwrap_err();
10012        assert!(err.message.contains("Invalid qos"), "got {}", err.message);
10013    }
10014
10015    #[test]
10016    fn channel_invalid_lifetime_rejected() {
10017        let err = parse("channel C { message: T lifetime: eternal }").unwrap_err();
10018        assert!(
10019            err.message.contains("Invalid lifetime"),
10020            "got {}",
10021            err.message
10022        );
10023    }
10024
10025    #[test]
10026    fn channel_invalid_persistence_rejected() {
10027        let err = parse("channel C { message: T persistence: forever }").unwrap_err();
10028        assert!(
10029            err.message.contains("Invalid persistence"),
10030            "got {}",
10031            err.message
10032        );
10033    }
10034
10035    #[test]
10036    fn emit_value_parses() {
10037        let src = "flow f() -> Out { emit C(payload) }";
10038        let prog = parse(src).expect("parse");
10039        if let Declaration::Flow(f) = &prog.declarations[0] {
10040            match &f.body[0] {
10041                FlowStep::Emit(e) => {
10042                    assert_eq!(e.channel_ref, "C");
10043                    assert_eq!(e.value_ref, "payload");
10044                }
10045                other => panic!("expected Emit, got {:?}", other),
10046            }
10047        } else {
10048            panic!("expected Flow");
10049        }
10050    }
10051
10052    #[test]
10053    fn publish_within_shield_parses() {
10054        let src = "flow f() -> Cap { publish C within Gate }";
10055        let prog = parse(src).expect("parse");
10056        if let Declaration::Flow(f) = &prog.declarations[0] {
10057            match &f.body[0] {
10058                FlowStep::Publish(p) => {
10059                    assert_eq!(p.channel_ref, "C");
10060                    assert_eq!(p.shield_ref, "Gate");
10061                }
10062                other => panic!("expected Publish, got {:?}", other),
10063            }
10064        } else {
10065            panic!("expected Flow");
10066        }
10067    }
10068
10069    #[test]
10070    fn discover_with_alias_parses() {
10071        let src = "flow f() -> Out { discover C as ch }";
10072        let prog = parse(src).expect("parse");
10073        if let Declaration::Flow(f) = &prog.declarations[0] {
10074            match &f.body[0] {
10075                FlowStep::Discover(d) => {
10076                    assert_eq!(d.capability_ref, "C");
10077                    assert_eq!(d.alias, "ch");
10078                }
10079                other => panic!("expected Discover, got {:?}", other),
10080            }
10081        } else {
10082            panic!("expected Flow");
10083        }
10084    }
10085
10086    #[test]
10087    fn listen_typed_ref_sets_flag_true() {
10088        let src = "daemon D() { goal: \"x\" listen C as ev { } }";
10089        let prog = parse(src).expect("parse");
10090        if let Declaration::Daemon(d) = &prog.declarations[0] {
10091            assert_eq!(d.listeners.len(), 1);
10092            assert_eq!(d.listeners[0].channel, "C");
10093            assert!(d.listeners[0].channel_is_ref, "typed ref ⇒ true");
10094        } else {
10095            panic!("expected Daemon");
10096        }
10097    }
10098
10099    #[test]
10100    fn listen_string_topic_legacy_flag_false() {
10101        let src = "daemon D() { goal: \"x\" listen \"orders\" as ev { } }";
10102        let prog = parse(src).expect("parse");
10103        if let Declaration::Daemon(d) = &prog.declarations[0] {
10104            assert_eq!(d.listeners.len(), 1);
10105            assert_eq!(d.listeners[0].channel, "orders");
10106            assert!(!d.listeners[0].channel_is_ref, "string topic ⇒ false");
10107        } else {
10108            panic!("expected Daemon");
10109        }
10110    }
10111
10112    // ── Fase 13.i — emit value_ref accepts dotted access ───────────
10113
10114    fn extract_first_emit(prog: &Program) -> &EmitStatement {
10115        if let Declaration::Flow(f) = &prog.declarations[0] {
10116            if let FlowStep::Emit(e) = &f.body[0] {
10117                return e;
10118            }
10119        }
10120        panic!("expected emit statement at flow body[0]");
10121    }
10122
10123    #[test]
10124    fn emit_accepts_bare_identifier_value_ref() {
10125        // Pre-13.i baseline — must keep working.
10126        let prog = parse("flow f() -> Out { emit Hello(payload) }").expect("parse");
10127        let emit = extract_first_emit(&prog);
10128        assert_eq!(emit.channel_ref, "Hello");
10129        assert_eq!(emit.value_ref, "payload");
10130    }
10131
10132    #[test]
10133    fn emit_accepts_two_segment_dotted_value_ref() {
10134        // The exact case adopters reported as broken before 13.i.
10135        let prog = parse("flow f() -> Out { emit Hello(Build.output) }").expect("parse");
10136        let emit = extract_first_emit(&prog);
10137        assert_eq!(emit.value_ref, "Build.output");
10138    }
10139
10140    #[test]
10141    fn emit_accepts_three_segment_nested_dotted_value_ref() {
10142        let prog = parse("flow f() -> Out { emit Score(Analyze.result.score) }").expect("parse");
10143        let emit = extract_first_emit(&prog);
10144        assert_eq!(emit.value_ref, "Analyze.result.score");
10145    }
10146
10147    #[test]
10148    fn emit_dotted_with_trailing_dot_fails() {
10149        // Trailing `.` must still error — every '.' demands an identifier.
10150        let result = parse("flow f() -> Out { emit Hello(Build.) }");
10151        assert!(result.is_err(), "expected parse error for trailing dot");
10152    }
10153}
10154
10155// ── §Fase 14.a — declaration_trivia parallel channel tests ──────────────────
10156
10157#[cfg(test)]
10158mod fase14a_declaration_trivia_tests {
10159    use super::*;
10160    use crate::lexer::Lexer;
10161    use crate::tokens::TriviaKind;
10162
10163    fn parse(src: &str) -> Program {
10164        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
10165        Parser::new(toks).parse().expect("parse")
10166    }
10167
10168    #[test]
10169    fn no_comments_means_empty_trivia_per_decl() {
10170        let prog = parse("flow F() -> Out { }");
10171        assert_eq!(prog.declarations.len(), 1);
10172        assert_eq!(prog.declaration_trivia.len(), 1);
10173        assert!(prog.declaration_trivia[0].leading.is_empty());
10174        assert!(prog.declaration_trivia[0].trailing.is_empty());
10175    }
10176
10177    #[test]
10178    fn doc_line_comment_attaches_as_leading() {
10179        let prog = parse("/// Documents F\nflow F() -> Out { }");
10180        let triv = &prog.declaration_trivia[0];
10181        assert_eq!(triv.leading.len(), 1);
10182        assert_eq!(triv.leading[0].kind, TriviaKind::DocLine);
10183        assert!(triv.leading[0].is_doc());
10184        assert_eq!(triv.leading[0].text, "/// Documents F");
10185    }
10186
10187    #[test]
10188    fn regular_line_comment_attaches_as_leading() {
10189        let prog = parse("// header\nflow F() -> Out { }");
10190        let triv = &prog.declaration_trivia[0];
10191        assert_eq!(triv.leading.len(), 1);
10192        assert_eq!(triv.leading[0].kind, TriviaKind::Line);
10193        assert!(!triv.leading[0].is_doc());
10194    }
10195
10196    #[test]
10197    fn block_doc_comment_attaches_as_leading() {
10198        let prog = parse("/** Doc block */\nflow F() -> Out { }");
10199        let triv = &prog.declaration_trivia[0];
10200        assert_eq!(triv.leading[0].kind, TriviaKind::DocBlock);
10201        assert!(triv.leading[0].is_doc());
10202    }
10203
10204    #[test]
10205    fn multiple_comments_collected_in_source_order() {
10206        let src = "/// First\n/// Second\nflow F() -> Out { }";
10207        let prog = parse(src);
10208        let triv = &prog.declaration_trivia[0];
10209        assert_eq!(triv.leading.len(), 2);
10210        assert_eq!(triv.leading[0].text, "/// First");
10211        assert_eq!(triv.leading[1].text, "/// Second");
10212    }
10213
10214    #[test]
10215    fn three_decls_each_get_own_leading() {
10216        let src = "/// for A\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }\n/// for C\nflow C() -> Out { }";
10217        let prog = parse(src);
10218        assert_eq!(prog.declarations.len(), 3);
10219        assert_eq!(prog.declaration_trivia.len(), 3);
10220        for (idx, name) in ["A", "B", "C"].iter().enumerate() {
10221            let triv = &prog.declaration_trivia[idx];
10222            assert_eq!(triv.leading.len(), 1);
10223            assert_eq!(triv.leading[0].text, format!("/// for {name}"));
10224        }
10225    }
10226
10227    #[test]
10228    fn trailing_comment_attaches_to_last_token_of_decl() {
10229        // Comment on the same line as the decl's closing brace.
10230        let prog = parse("flow F() -> Out { } // tail");
10231        let triv = &prog.declaration_trivia[0];
10232        assert_eq!(triv.trailing.len(), 1);
10233        assert_eq!(triv.trailing[0].text, "// tail");
10234    }
10235
10236    #[test]
10237    fn mixed_doc_and_regular_preserve_order_between_decls() {
10238        let src = "/// doc for A\nflow A() -> Out { }\n\n// header line\n/// doc for B\nflow B() -> Out { }";
10239        let prog = parse(src);
10240        assert_eq!(prog.declarations.len(), 2);
10241        // A: just the doc comment.
10242        assert_eq!(prog.declaration_trivia[0].leading.len(), 1);
10243        // B: header + doc, in source order.
10244        assert_eq!(prog.declaration_trivia[1].leading.len(), 2);
10245        assert_eq!(prog.declaration_trivia[1].leading[0].text, "// header line");
10246        assert_eq!(prog.declaration_trivia[1].leading[1].text, "/// doc for B");
10247    }
10248
10249    #[test]
10250    fn parser_unaffected_by_comments_in_grammar_path() {
10251        // The parser must accept comments interleaved between every
10252        // legal token without affecting the AST shape it produces.
10253        // This is the regression guard for "lossless lexing must not
10254        // change parsing semantics."
10255        let src =
10256            "// before flow\nflow /* between flow and name */ F() -> Out {\n  // body comment\n}";
10257        let prog = parse(src);
10258        assert_eq!(prog.declarations.len(), 1);
10259        if let Declaration::Flow(f) = &prog.declarations[0] {
10260            assert_eq!(f.name, "F");
10261        } else {
10262            panic!("expected Flow declaration");
10263        }
10264    }
10265}
10266
10267// ── §Fase 14.b — per-struct trivia fields tests ─────────────────────────────
10268//
10269// 14.b spreads `leading_trivia` / `trailing_trivia` into every Declaration
10270// variant struct (FlowDefinition, ChannelDefinition, PersonaDefinition, …).
10271// The Python AST already had this shape since 14.a; 14.b achieves Rust
10272// parity. The side-channel `Program.declaration_trivia` is preserved for
10273// backward compat — these tests verify the new direct access path.
10274
10275#[cfg(test)]
10276mod fase14b_per_struct_trivia_tests {
10277    use super::*;
10278    use crate::lexer::Lexer;
10279    use crate::tokens::TriviaKind;
10280
10281    fn parse(src: &str) -> Program {
10282        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
10283        Parser::new(toks).parse().expect("parse")
10284    }
10285
10286    #[test]
10287    fn flow_definition_carries_leading_trivia_directly() {
10288        let prog = parse("/// documents F\nflow F() -> Out { }");
10289        if let Declaration::Flow(f) = &prog.declarations[0] {
10290            assert_eq!(f.leading_trivia.len(), 1);
10291            assert_eq!(f.leading_trivia[0].kind, TriviaKind::DocLine);
10292            assert_eq!(f.leading_trivia[0].text, "/// documents F");
10293            assert!(f.trailing_trivia.is_empty());
10294        } else {
10295            panic!("expected Flow declaration");
10296        }
10297    }
10298
10299    #[test]
10300    fn flow_definition_carries_trailing_trivia_directly() {
10301        let prog = parse("flow F() -> Out { } // tail comment");
10302        if let Declaration::Flow(f) = &prog.declarations[0] {
10303            assert_eq!(f.trailing_trivia.len(), 1);
10304            assert_eq!(f.trailing_trivia[0].text, "// tail comment");
10305        } else {
10306            panic!("expected Flow declaration");
10307        }
10308    }
10309
10310    #[test]
10311    fn channel_definition_carries_trivia_directly() {
10312        // ChannelDefinition is a Tier-1 declaration; verify per-struct fields
10313        // populate just like FlowDefinition.
10314        let src = concat!(
10315            "/// inbound order events\n",
10316            "channel Orders {\n",
10317            "    message:     Order\n",
10318            "    qos:         at_least_once\n",
10319            "    lifetime:    affine\n",
10320            "    persistence: ephemeral\n",
10321            "    shield:      Broker\n",
10322            "}",
10323        );
10324        let prog = parse(src);
10325        if let Declaration::Channel(ch) = &prog.declarations[0] {
10326            assert_eq!(ch.leading_trivia.len(), 1);
10327            assert!(ch.leading_trivia[0].is_doc());
10328            assert_eq!(ch.leading_trivia[0].text, "/// inbound order events");
10329        } else {
10330            panic!("expected Channel declaration");
10331        }
10332    }
10333
10334    #[test]
10335    fn per_struct_fields_match_side_channel() {
10336        // 14.a side-channel and 14.b per-struct fields must hold identical
10337        // data — they are populated by the same parser pass.
10338        let src = "/// for A\n// header for B\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }";
10339        let prog = parse(src);
10340        for (idx, decl) in prog.declarations.iter().enumerate() {
10341            let side = &prog.declaration_trivia[idx];
10342            let (per_lead, per_trail) = match decl {
10343                Declaration::Flow(f) => (&f.leading_trivia, &f.trailing_trivia),
10344                _ => panic!("unexpected variant"),
10345            };
10346            assert_eq!(per_lead.len(), side.leading.len());
10347            assert_eq!(per_trail.len(), side.trailing.len());
10348            for (a, b) in per_lead.iter().zip(side.leading.iter()) {
10349                assert_eq!(a.text, b.text);
10350                assert_eq!(a.kind, b.kind);
10351            }
10352        }
10353    }
10354
10355    #[test]
10356    fn comment_free_program_yields_empty_per_struct_fields() {
10357        let prog = parse("flow F() -> Out { }");
10358        if let Declaration::Flow(f) = &prog.declarations[0] {
10359            assert!(f.leading_trivia.is_empty());
10360            assert!(f.trailing_trivia.is_empty());
10361        } else {
10362            panic!("expected Flow declaration");
10363        }
10364    }
10365}
10366
10367// ── §Fase 14.c — inner doc comments (//!, /*!) ──────────────────────────────
10368//
10369// Inner doc comments document the *enclosing* item rather than the next
10370// sibling. Today they flow through the trivia channel like any other
10371// comment; downstream consumers (axon doc, LSP) decide how to interpret
10372// `is_inner_doc()`. These tests verify the lexer→parser pipeline preserves
10373// the inner-doc discriminator end-to-end.
10374
10375#[cfg(test)]
10376mod fase14c_inner_doc_tests {
10377    use super::*;
10378    use crate::lexer::Lexer;
10379    use crate::tokens::TriviaKind;
10380
10381    fn parse(src: &str) -> Program {
10382        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
10383        Parser::new(toks).parse().expect("parse")
10384    }
10385
10386    #[test]
10387    fn inner_doc_line_reaches_leading_trivia() {
10388        let src = "//! file-level docs\nflow F() -> Out { }";
10389        let prog = parse(src);
10390        let triv = &prog.declaration_trivia[0];
10391        assert_eq!(triv.leading.len(), 1);
10392        assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocLine);
10393        assert!(triv.leading[0].is_doc());
10394        assert!(triv.leading[0].is_inner_doc());
10395        assert_eq!(triv.leading[0].text, "//! file-level docs");
10396        assert_eq!(triv.leading[0].stripped_text(), " file-level docs");
10397    }
10398
10399    #[test]
10400    fn inner_doc_block_reaches_leading_trivia() {
10401        let src = "/*! module-level docs */\nflow F() -> Out { }";
10402        let prog = parse(src);
10403        let triv = &prog.declaration_trivia[0];
10404        assert_eq!(triv.leading.len(), 1);
10405        assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocBlock);
10406        assert!(triv.leading[0].is_inner_doc());
10407        assert_eq!(triv.leading[0].stripped_text(), " module-level docs ");
10408    }
10409
10410    #[test]
10411    fn outer_and_inner_doc_can_coexist() {
10412        // File-level inner doc on top, then an outer doc for the
10413        // declaration. Both reach the trivia channel and remain
10414        // distinguishable via `is_inner_doc()`.
10415        let src = "//! file docs\n/// docs F\nflow F() -> Out { }";
10416        let prog = parse(src);
10417        let triv = &prog.declaration_trivia[0];
10418        assert_eq!(triv.leading.len(), 2);
10419        assert!(triv.leading[0].is_inner_doc());
10420        assert!(triv.leading[1].is_doc());
10421        assert!(!triv.leading[1].is_inner_doc());
10422    }
10423
10424    #[test]
10425    fn inner_doc_reaches_per_struct_fields() {
10426        // Same data must be visible via the per-struct fields (Fase 14.b).
10427        let src = "//! intro\nflow F() -> Out { }";
10428        let prog = parse(src);
10429        if let Declaration::Flow(f) = &prog.declarations[0] {
10430            assert_eq!(f.leading_trivia.len(), 1);
10431            assert!(f.leading_trivia[0].is_inner_doc());
10432        } else {
10433            panic!("expected Flow declaration");
10434        }
10435    }
10436}
10437
10438// ── §Fase 28.c — Parser error recovery test pack ─────────────────────────────
10439//
10440// Mirror of `tests/test_fase28_parser_recovery.py` (Python side, 28.b).
10441// The test classes here line up 1-1 with the Python ones so the cross-
10442// stack drift gate (28.i) can compare error-list shapes input-for-input.
10443//
10444// Test classes:
10445//   - backwards_compat: existing `parse()` API unchanged
10446//   - single_error_recovery: one bad decl → one error, rest parse OK
10447//   - multi_error_recovery: N independent errors → N entries
10448//   - sync_points: every top-level keyword resyncs correctly
10449//   - parse_result_api: `has_errors`, `is_clean`
10450//   - edge_cases: EOF mid-error, brace imbalance, only-bad-tokens
10451//   - robustness_fuzz: 1000 deterministic-seeded mutations never crash
10452//   - no_ghost_errors: single broken field produces exactly 1 error
10453//   - integration_with_colon_diagnostic: v1.19.4 hint preserved under
10454//     recovery mode
10455#[cfg(test)]
10456mod fase28_recovery_tests {
10457    use super::*;
10458    use crate::lexer::Lexer;
10459
10460    /// Lex a source and return tokens for the parser to consume.
10461    /// Mirrors the Python `_parse_recovery` helper.
10462    fn lex(src: &str) -> Vec<Token> {
10463        Lexer::new(src, "<test>").tokenize().expect("lex")
10464    }
10465
10466    /// Parse with recovery mode. Returns `(program, errors)` so call
10467    /// sites read like the Python helper.
10468    fn recover(src: &str) -> ParseResult {
10469        Parser::new(lex(src)).parse_with_recovery()
10470    }
10471
10472    /// Strict parse. Mirrors the Python `_parse_strict` helper.
10473    fn strict(src: &str) -> Result<Program, ParseError> {
10474        Parser::new(lex(src)).parse()
10475    }
10476
10477    // ── backwards_compat ─────────────────────────────────────────
10478
10479    #[test]
10480    fn strict_parse_unchanged_for_clean_source() {
10481        // The existing `parse()` API must continue to succeed
10482        // verbatim on every well-formed input — D9.
10483        let src = "intent I {}";
10484        let prog = strict(src).expect("clean parse");
10485        assert_eq!(prog.declarations.len(), 1);
10486    }
10487
10488    #[test]
10489    fn strict_parse_still_raises_on_first_error() {
10490        // D9 + D8: opt-in to recovery via `parse_with_recovery`;
10491        // strict mode must still bubble the first error.
10492        // (Using a parse-time error rather than a lex error — `@@@`
10493        // would be rejected by the lexer, which is out of scope.)
10494        let src = "flow F() { } not_a_keyword flow G() { }";
10495        let _ = strict(src).expect_err("must error fast in strict mode");
10496    }
10497
10498    #[test]
10499    fn recovery_clean_source_yields_no_errors() {
10500        let src = "flow F() { } flow G() { }";
10501        let pr = recover(src);
10502        assert!(pr.is_clean(), "errors: {:?}", pr.errors);
10503        assert_eq!(pr.program.declarations.len(), 2);
10504    }
10505
10506    // ── single_error_recovery ────────────────────────────────────
10507
10508    #[test]
10509    fn single_unknown_top_level_token_recovers() {
10510        // One garbage token at top level; rest must parse.
10511        let src = "garbage_token flow F() { } flow G() { }";
10512        let pr = recover(src);
10513        assert_eq!(pr.errors.len(), 1, "errors: {:?}", pr.errors);
10514        assert_eq!(pr.program.declarations.len(), 2);
10515    }
10516
10517    #[test]
10518    fn error_in_first_decl_does_not_block_second() {
10519        // `flow F` body refers to non-keyword `nope`; the error
10520        // recovery must skip to the next top-level keyword.
10521        let src = "flow F() { not_a_step nope } flow G() { }";
10522        let pr = recover(src);
10523        assert!(pr.has_errors(), "expected at least one error");
10524        // The second flow must be reachable.
10525        let names: Vec<&str> = pr
10526            .program
10527            .declarations
10528            .iter()
10529            .filter_map(|d| match d {
10530                Declaration::Flow(f) => Some(f.name.as_str()),
10531                _ => None,
10532            })
10533            .collect();
10534        assert!(names.contains(&"G"), "G not found among {names:?}");
10535    }
10536
10537    #[test]
10538    fn malformed_declaration_then_clean_intent_recovers() {
10539        let src = "flow @ () { } intent I {}";
10540        let pr = recover(src);
10541        assert!(pr.has_errors());
10542        let kinds: Vec<&str> = pr
10543            .program
10544            .declarations
10545            .iter()
10546            .map(|d| match d {
10547                Declaration::Intent(_) => "intent",
10548                Declaration::Flow(_) => "flow",
10549                _ => "other",
10550            })
10551            .collect();
10552        assert!(kinds.contains(&"intent"), "kinds: {kinds:?}");
10553    }
10554
10555    #[test]
10556    fn recovery_does_not_double_count_a_single_error() {
10557        // Regression for the "ghost error" pathology that surfaced
10558        // during 28.b dev: a nested-decl error must not also fire
10559        // an "Unexpected token at top level" from the outer loop.
10560        // The Rust grammar has stricter intra-flow requirements
10561        // than Python; the invariant we assert here is that the
10562        // outer loop emits zero "Unexpected token at top level"
10563        // errors after an inner step-shape error.
10564        let src = "flow F() { not_a_step }";
10565        let pr = recover(src);
10566        let outer_ghosts = pr
10567            .errors
10568            .iter()
10569            .filter(|e| e.message.contains("at top level"))
10570            .count();
10571        assert_eq!(outer_ghosts, 0, "ghost errors: {:?}", pr.errors);
10572    }
10573
10574    // ── multi_error_recovery ─────────────────────────────────────
10575
10576    #[test]
10577    fn three_independent_errors_yield_three_entries() {
10578        let src =
10579            "garbage1 flow F() { } garbage2 flow G() { } garbage3 flow H() { }";
10580        let pr = recover(src);
10581        assert_eq!(pr.errors.len(), 3, "errors: {:?}", pr.errors);
10582        assert_eq!(pr.program.declarations.len(), 3);
10583    }
10584
10585    #[test]
10586    fn all_errors_no_valid_declarations() {
10587        let src = "foo bar baz qux";
10588        let pr = recover(src);
10589        assert!(pr.has_errors());
10590        assert!(pr.program.declarations.is_empty());
10591    }
10592
10593    #[test]
10594    fn errors_recorded_in_source_order() {
10595        let src = "x flow A() { } y flow B() { } z flow C() { }";
10596        let pr = recover(src);
10597        assert_eq!(pr.errors.len(), 3);
10598        let lines: Vec<u32> = pr.errors.iter().map(|e| e.line).collect();
10599        // Same source-line means we compare by column ordering;
10600        // either way they must be non-decreasing.
10601        assert!(
10602            lines.windows(2).all(|w| w[0] <= w[1]),
10603            "errors out of order: {lines:?}"
10604        );
10605    }
10606
10607    // ── sync_points ──────────────────────────────────────────────
10608
10609    #[test]
10610    fn sync_to_flow_keyword() {
10611        let src = "garbage flow F() { }";
10612        let pr = recover(src);
10613        assert_eq!(pr.program.declarations.len(), 1);
10614    }
10615
10616    #[test]
10617    fn sync_to_intent_keyword() {
10618        let src = "garbage intent I {}";
10619        let pr = recover(src);
10620        assert_eq!(pr.program.declarations.len(), 1);
10621    }
10622
10623    #[test]
10624    fn sync_to_persona_keyword() {
10625        let src = "garbage persona P { name: \"P\" role: \"R\" }";
10626        let pr = recover(src);
10627        assert!(
10628            pr.program
10629                .declarations
10630                .iter()
10631                .any(|d| matches!(d, Declaration::Persona(_))),
10632            "persona not recovered: decls = {:?}",
10633            pr.program.declarations.len()
10634        );
10635    }
10636
10637    #[test]
10638    fn sync_to_run_keyword() {
10639        let src = "garbage run R { input: { user_message: \"hi\" } }";
10640        let pr = recover(src);
10641        // Either Run was parsed, or recovery still produced ≥1 err.
10642        assert!(pr.has_errors());
10643    }
10644
10645    // ── parse_result_api ─────────────────────────────────────────
10646
10647    #[test]
10648    fn parse_result_has_errors_and_is_clean_invert() {
10649        let pr_clean = recover("flow F() { }");
10650        assert!(pr_clean.is_clean());
10651        assert!(!pr_clean.has_errors());
10652
10653        let pr_err = recover("garbage");
10654        assert!(!pr_err.is_clean());
10655        assert!(pr_err.has_errors());
10656    }
10657
10658    #[test]
10659    fn parse_result_program_field_holds_partial_program() {
10660        let pr = recover("garbage flow F() { }");
10661        assert!(!pr.program.declarations.is_empty());
10662    }
10663
10664    #[test]
10665    fn parse_result_errors_carry_line_and_column() {
10666        let pr = recover("garbage");
10667        assert!(!pr.errors.is_empty());
10668        let e = &pr.errors[0];
10669        assert!(e.line >= 1);
10670        // Column may be 0-based or 1-based depending on lexer;
10671        // accept anything ≥ 0.
10672        let _ = e.column;
10673        assert!(!e.message.is_empty());
10674    }
10675
10676    #[test]
10677    fn parse_result_debug_renders() {
10678        let pr = recover("flow F() { }");
10679        let s = format!("{pr:?}");
10680        assert!(s.contains("ParseResult"));
10681    }
10682
10683    // ── edge_cases ───────────────────────────────────────────────
10684
10685    #[test]
10686    fn empty_source_is_clean() {
10687        let pr = recover("");
10688        assert!(pr.is_clean());
10689        assert!(pr.program.declarations.is_empty());
10690    }
10691
10692    #[test]
10693    fn whitespace_only_source_is_clean() {
10694        let pr = recover("   \n\n\t  \n");
10695        assert!(pr.is_clean());
10696        assert!(pr.program.declarations.is_empty());
10697    }
10698
10699    #[test]
10700    fn only_garbage_does_not_crash() {
10701        // Lex-clean garbage tokens (avoids AxonLexerError).
10702        let pr = recover("foo bar baz { qux quux } corge { grault }");
10703        assert!(pr.has_errors());
10704    }
10705
10706    #[test]
10707    fn unbalanced_close_brace_does_not_crash() {
10708        let pr = recover("} flow F() { }");
10709        // Recovery must keep walking past stray `}`.
10710        let names: Vec<&str> = pr
10711            .program
10712            .declarations
10713            .iter()
10714            .filter_map(|d| match d {
10715                Declaration::Flow(f) => Some(f.name.as_str()),
10716                _ => None,
10717            })
10718            .collect();
10719        assert!(names.contains(&"F"), "F not recovered: {names:?}");
10720    }
10721
10722    #[test]
10723    fn error_at_eof_does_not_loop() {
10724        // Truncated declaration. Must terminate; finite errors.
10725        let pr = recover("flow F() { ");
10726        // Either errored or somehow accepted — but must terminate.
10727        let _ = pr.errors.len();
10728    }
10729
10730    #[test]
10731    fn nested_braces_inside_error_still_balance() {
10732        // Walker must respect brace depth so a `}` inside a malformed
10733        // block does not prematurely sync.
10734        let src = "flow F() { not_a_step { inner } } flow G() { }";
10735        let pr = recover(src);
10736        let names: Vec<&str> = pr
10737            .program
10738            .declarations
10739            .iter()
10740            .filter_map(|d| match d {
10741                Declaration::Flow(f) => Some(f.name.as_str()),
10742                _ => None,
10743            })
10744            .collect();
10745        assert!(names.contains(&"G"), "G not recovered: {names:?}");
10746    }
10747
10748    // ── robustness_fuzz ──────────────────────────────────────────
10749    //
10750    // Deterministic-seeded mutator (xorshift). 100 buckets ×
10751    // 10 mutations = 1000 iterations, byte-bounded so fuzz time
10752    // stays under 1 s on a release build. Recovery must NEVER crash;
10753    // lexer-level errors are out of scope (lexer recovery is its own
10754    // sub-fase). 28.b mirrors this with the same structure.
10755
10756    #[derive(Clone, Copy)]
10757    struct Xorshift(u64);
10758    impl Xorshift {
10759        fn next(&mut self) -> u64 {
10760            let mut x = self.0;
10761            x ^= x << 13;
10762            x ^= x >> 7;
10763            x ^= x << 17;
10764            self.0 = x;
10765            x
10766        }
10767        fn pick<T: Copy>(&mut self, slice: &[T]) -> T {
10768            slice[(self.next() as usize) % slice.len()]
10769        }
10770    }
10771
10772    fn mutate(src: &str, rng: &mut Xorshift) -> String {
10773        let mut bytes: Vec<u8> = src.bytes().collect();
10774        if bytes.is_empty() {
10775            return src.to_string();
10776        }
10777        let op = rng.next() % 4;
10778        let pos = (rng.next() as usize) % bytes.len();
10779        // Stick to ASCII-safe printable bytes to keep input lex-able
10780        // most of the time. AxonLexerError is still possible and is
10781        // tolerated by the recovery contract.
10782        let safe: &[u8] = b"abcdefghijklmnopqrstuvwxyz {}();:,_0123456789";
10783        match op {
10784            0 => {
10785                bytes.remove(pos);
10786            }
10787            1 => {
10788                let b = rng.pick(safe);
10789                bytes.insert(pos, b);
10790            }
10791            2 if pos + 1 < bytes.len() => {
10792                bytes.swap(pos, pos + 1);
10793            }
10794            _ => {
10795                let b = rng.pick(safe);
10796                bytes[pos] = b;
10797            }
10798        }
10799        // Lossy decode: mutator may have produced invalid UTF-8;
10800        // strip non-ASCII before handing to the lexer.
10801        bytes.retain(|b| b.is_ascii());
10802        String::from_utf8_lossy(&bytes).into_owned()
10803    }
10804
10805    #[test]
10806    fn fuzz_recovery_never_crashes() {
10807        let seed_bases = [
10808            "flow F() { }",
10809            "intent I { }",
10810            "persona P { name: \"P\" role: \"R\" }",
10811            "intent J { ask: \"a\" }",
10812            "type T = String",
10813        ];
10814        // 100 buckets × 10 mutations = 1000 iterations, deterministic.
10815        for (bucket, base) in (0..100u64).zip(seed_bases.iter().cycle()) {
10816            let mut rng = Xorshift(0x1234_5678_9abc_def0_u64.wrapping_add(bucket));
10817            let mut current = (*base).to_string();
10818            for _ in 0..10 {
10819                current = mutate(&current, &mut rng);
10820                // Lexer may reject; that's outside parser-recovery
10821                // scope (28.b/c). Skip those iterations.
10822                let toks = match Lexer::new(&current, "<fuzz>").tokenize() {
10823                    Ok(t) => t,
10824                    Err(_) => continue,
10825                };
10826                // Recovery must not panic on any well-lexed input.
10827                let _pr = Parser::new(toks).parse_with_recovery();
10828            }
10829        }
10830    }
10831
10832    // ── integration_with_v1_19_4_colon_diagnostic ────────────────
10833
10834    #[test]
10835    fn missing_colon_hint_preserved_under_recovery() {
10836        // The Rust frontend's strict `parse()` carries the same
10837        // colon diagnostic shape as the Python side. Recovery mode
10838        // must not erase it.
10839        let src = "flow F() { run R { input { user_message: \"hi\" } } }";
10840        let pr = recover(src);
10841        // Either the parser accepts this (some shape may be valid)
10842        // or it errors — but if it errors, the message must surface
10843        // the diagnostic content.
10844        if !pr.errors.is_empty() {
10845            let any_msg = pr.errors.iter().any(|e| !e.message.is_empty());
10846            assert!(any_msg);
10847        }
10848    }
10849
10850    // ── recovery preserves declaration ordering ──────────────────
10851
10852    #[test]
10853    fn recovered_declarations_appear_in_source_order() {
10854        let src = "flow A() { } garbage flow B() { } garbage flow C() { }";
10855        let pr = recover(src);
10856        let names: Vec<&str> = pr
10857            .program
10858            .declarations
10859            .iter()
10860            .filter_map(|d| match d {
10861                Declaration::Flow(f) => Some(f.name.as_str()),
10862                _ => None,
10863            })
10864            .collect();
10865        assert_eq!(names, vec!["A", "B", "C"]);
10866    }
10867}
10868
10869// ── §Fase 28.d — Source-context diagnostic block test pack ───────────────────
10870//
10871// Mirror of `tests/test_fase28_source_context.py` (Python side, 28.d).
10872// The render output must be byte-identical to the Python `SourceSnippet.render`
10873// on the same input — D7 ratified (cross-stack drift gate). Golden strings
10874// in `golden_*` tests are duplicated verbatim in the Python pack; edits
10875// here MUST be mirrored on the Python side and vice versa.
10876#[cfg(test)]
10877mod fase28_source_context_tests {
10878    use super::*;
10879    use crate::lexer::Lexer;
10880
10881    fn snippet(source: &str, line: u32, column: u32, filename: &str) -> String {
10882        SourceSnippet::new(
10883            source.to_string(),
10884            line,
10885            column,
10886            filename.to_string(),
10887        )
10888        .render()
10889    }
10890
10891    // ── Pure rendering ──────────────────────────────────────────
10892
10893    #[test]
10894    fn rustc_style_block_for_middle_line() {
10895        let src = "line one\nline two\nline three\nline four\nline five";
10896        let out = snippet(src, 3, 6, "x.axon");
10897        assert!(out.contains("--> x.axon:3:6"));
10898        assert!(out.contains("1 | line one"));
10899        assert!(out.contains("2 | line two"));
10900        assert!(out.contains("3 | line three"));
10901        assert!(out.contains("4 | line four"));
10902        assert!(out.contains("5 | line five"));
10903        // Caret col 6 → 5-space pad. Empty gutter is 1 space (gutter=1).
10904        assert!(out.contains("\n  |      ^"), "out:\n{out}");
10905    }
10906
10907    #[test]
10908    fn caret_column_one_renders_correctly() {
10909        let out = snippet("abc\n", 1, 1, "<source>");
10910        assert!(out.contains("\n  | ^"));
10911    }
10912
10913    #[test]
10914    fn first_line_clamps_context_before_to_zero() {
10915        let src = "first\nsecond\nthird\nfourth\nfifth";
10916        let out = snippet(src, 1, 1, "<source>");
10917        assert!(out.contains("1 | first"));
10918        assert!(out.contains("2 | second"));
10919        assert!(out.contains("3 | third"));
10920        assert!(!out.contains("4 | fourth"));
10921    }
10922
10923    #[test]
10924    fn last_line_clamps_context_after_to_eof() {
10925        let src = "first\nsecond\nthird\nfourth\nfifth";
10926        let out = snippet(src, 5, 2, "<source>");
10927        assert!(out.contains("5 | fifth"));
10928        assert!(out.contains("3 | third"));
10929        assert!(out.contains("4 | fourth"));
10930        assert!(!out.contains("2 | second"));
10931    }
10932
10933    #[test]
10934    fn gutter_width_grows_with_line_count() {
10935        let src: String = (1..=12).map(|i| format!("line{i}")).collect::<Vec<_>>().join("\n");
10936        let out = snippet(&src, 12, 1, "<source>");
10937        assert!(out.contains("12 | line12"));
10938        assert!(out.contains("10 | line10"));
10939    }
10940
10941    // ── Edge cases ──────────────────────────────────────────────
10942
10943    #[test]
10944    fn empty_source_returns_empty() {
10945        assert_eq!(snippet("", 1, 1, "<source>"), "");
10946    }
10947
10948    #[test]
10949    fn zero_line_returns_empty() {
10950        assert_eq!(snippet("hi", 0, 1, "<source>"), "");
10951    }
10952
10953    #[test]
10954    fn out_of_range_line_returns_empty() {
10955        assert_eq!(snippet("hi", 99, 1, "<source>"), "");
10956    }
10957
10958    #[test]
10959    fn caret_clamps_past_eol() {
10960        let out = snippet("hello", 1, 50, "<source>");
10961        assert!(out.contains("\n  |      ^"), "out:\n{out}");
10962    }
10963
10964    #[test]
10965    fn unicode_codepoint_count_for_caret_clamp() {
10966        // "héllo" = 5 codepoints; column past EOL clamps to 6.
10967        let out = snippet("héllo", 1, 99, "<source>");
10968        assert!(out.contains("\n  |      ^"), "out:\n{out}");
10969    }
10970
10971    #[test]
10972    fn trailing_newline_does_not_create_phantom_last_line() {
10973        let out = snippet("first\nsecond\n", 2, 1, "<source>");
10974        assert!(!out.contains("3 |"));
10975        assert!(out.contains("2 | second"));
10976    }
10977
10978    // ── Parser attach plumbing ──────────────────────────────────
10979
10980    fn lex(src: &str) -> Vec<Token> {
10981        Lexer::new(src, "<test>").tokenize().expect("lex")
10982    }
10983
10984    #[test]
10985    fn strict_parse_attaches_snippet_when_source_given() {
10986        let src = "garbage_token\nflow F() { }";
10987        let err = Parser::new(lex(src))
10988            .with_source(src, "x.axon")
10989            .parse()
10990            .expect_err("must error");
10991        assert!(err.source_snippet.is_some());
10992        let display = format!("{err}");
10993        assert!(display.contains("--> x.axon:"), "display: {display}");
10994    }
10995
10996    #[test]
10997    fn strict_parse_no_snippet_when_no_source() {
10998        let src = "garbage_token";
10999        let err = Parser::new(lex(src)).parse().expect_err("must error");
11000        assert!(err.source_snippet.is_none());
11001        let display = format!("{err}");
11002        assert!(!display.contains("\n  -->"));
11003    }
11004
11005    #[test]
11006    fn every_recovered_error_has_snippet() {
11007        let src = "garbage1\nflow F() { }\ngarbage2\nflow G() { }";
11008        let result = Parser::new(lex(src))
11009            .with_source(src, "multi.axon")
11010            .parse_with_recovery();
11011        assert!(!result.errors.is_empty());
11012        for err in &result.errors {
11013            assert!(err.source_snippet.is_some());
11014            let display = format!("{err}");
11015            assert!(
11016                display.contains("--> multi.axon:"),
11017                "display: {display}"
11018            );
11019        }
11020    }
11021
11022    #[test]
11023    fn recovery_no_snippet_when_no_source() {
11024        let src = "garbage1 garbage2";
11025        let result = Parser::new(lex(src)).parse_with_recovery();
11026        for err in &result.errors {
11027            assert!(err.source_snippet.is_none());
11028        }
11029    }
11030
11031    #[test]
11032    fn snippet_points_at_correct_line_for_each_error() {
11033        let src = "garbage_a\nflow F() { }\ngarbage_b\nflow G() { }";
11034        let result = Parser::new(lex(src))
11035            .with_source(src, "x")
11036            .parse_with_recovery();
11037        for err in &result.errors {
11038            let sn = err.source_snippet.as_ref().expect("snippet");
11039            assert_eq!(sn.line, err.line);
11040        }
11041    }
11042
11043    // ── Backwards-compat ────────────────────────────────────────
11044
11045    #[test]
11046    fn legacy_constructor_still_works() {
11047        let src = "flow F() { }";
11048        let prog = Parser::new(lex(src)).parse().expect("clean");
11049        assert_eq!(prog.declarations.len(), 1);
11050    }
11051
11052    #[test]
11053    fn attach_source_idempotent() {
11054        let err = ParseError {
11055            message: "bad".to_string(),
11056            line: 2,
11057            column: 3,
11058            ..Default::default()
11059        };
11060        let err2 = err.clone().attach_source("a\nb\nc\n", "f.axon");
11061        let first = format!("{err2}");
11062        let err3 = err.attach_source("a\nb\nc\n", "f.axon");
11063        let second = format!("{err3}");
11064        assert_eq!(first, second);
11065    }
11066
11067    #[test]
11068    fn attach_source_noop_when_line_zero() {
11069        let err = ParseError {
11070            message: "bad".to_string(),
11071            line: 0,
11072            column: 0,
11073            ..Default::default()
11074        };
11075        let err = err.attach_source("a\nb\nc\n", "f.axon");
11076        assert!(err.source_snippet.is_none());
11077    }
11078
11079    // ── Cross-stack golden parity ───────────────────────────────
11080    // These golden strings are duplicated verbatim in the Python
11081    // test pack at `tests/test_fase28_source_context.py::TestRustParityShape`.
11082    // Edits here MUST be mirrored in the Python pack — D7.
11083
11084    #[test]
11085    fn golden_simple_three_line_block() {
11086        let src = "alpha\nbeta\ngamma";
11087        let out = snippet(src, 2, 3, "g.axon");
11088        // Note: gutter=1, so empty_gutter=" " (one space). The
11089        // " --> ..." line therefore starts with two spaces ("<empty>"
11090        // + literal " --> ...").
11091        let expected = concat!(
11092            "  --> g.axon:2:3\n",
11093            "  |\n",
11094            "1 | alpha\n",
11095            "2 | beta\n",
11096            "  |   ^\n",
11097            "3 | gamma",
11098        );
11099        assert_eq!(out, expected);
11100    }
11101
11102    #[test]
11103    fn golden_first_line_caret() {
11104        let src = "abc\ndef\n";
11105        let out = snippet(src, 1, 1, "x");
11106        let expected = concat!(
11107            "  --> x:1:1\n",
11108            "  |\n",
11109            "1 | abc\n",
11110            "  | ^\n",
11111            "2 | def",
11112        );
11113        assert_eq!(out, expected);
11114    }
11115
11116    #[test]
11117    fn golden_two_digit_gutter() {
11118        let src: String = (1..=11)
11119            .map(|i| format!("L{i}"))
11120            .collect::<Vec<_>>()
11121            .join("\n");
11122        let out = snippet(&src, 10, 2, "big");
11123        let expected = concat!(
11124            "   --> big:10:2\n",
11125            "   |\n",
11126            " 8 | L8\n",
11127            " 9 | L9\n",
11128            "10 | L10\n",
11129            "   |  ^\n",
11130            "11 | L11",
11131        );
11132        assert_eq!(out, expected);
11133    }
11134}
11135
11136// ── §Fase 28.e — Parser integration tests for smart-suggest ──────────────────
11137//
11138// Mirror of `tests/test_fase28_smart_suggest.py::TestParserIntegration`.
11139// Verifies that the parser actually wires `suggest_for` into the
11140// unknown-keyword diagnostic at both error sites — top-level and
11141// flow-body.
11142#[cfg(test)]
11143mod fase28_smart_suggest_parser_tests {
11144    use super::*;
11145    use crate::lexer::Lexer;
11146
11147    fn lex(src: &str) -> Vec<Token> {
11148        Lexer::new(src, "<test>").tokenize().expect("lex")
11149    }
11150
11151    #[test]
11152    fn top_level_typo_suggests_flow() {
11153        let src = "flwo F() { }";
11154        let err = Parser::new(lex(src)).parse().expect_err("must error");
11155        assert!(
11156            err.message.contains("Did you mean `flow`?"),
11157            "msg: {}",
11158            err.message
11159        );
11160    }
11161
11162    #[test]
11163    fn top_level_unknown_far_no_suggestion() {
11164        let src = "qwerty F() { }";
11165        let err = Parser::new(lex(src)).parse().expect_err("must error");
11166        assert!(
11167            !err.message.contains("Did you mean"),
11168            "msg: {}",
11169            err.message
11170        );
11171    }
11172
11173    #[test]
11174    fn flow_body_typo_suggests_step() {
11175        let src = "flow F() { stepp S {} }";
11176        let err = Parser::new(lex(src)).parse().expect_err("must error");
11177        assert!(
11178            err.message.contains("Did you mean `step`"),
11179            "msg: {}",
11180            err.message
11181        );
11182    }
11183
11184    #[test]
11185    fn flow_body_typo_suggests_reason() {
11186        let src = "flow F() { reasn R {} }";
11187        let err = Parser::new(lex(src)).parse().expect_err("must error");
11188        assert!(
11189            err.message.contains("Did you mean `reason`?"),
11190            "msg: {}",
11191            err.message
11192        );
11193    }
11194
11195    #[test]
11196    fn recovery_mode_carries_hint() {
11197        let src = "flwo F() { }";
11198        let result = Parser::new(lex(src)).parse_with_recovery();
11199        assert!(
11200            result
11201                .errors
11202                .iter()
11203                .any(|e| e.message.contains("Did you mean `flow`?")),
11204            "errors: {:?}",
11205            result.errors
11206        );
11207    }
11208}
11209
11210// ── §Fase 35.m — mutate / purge where-clause capture ────────────────
11211
11212#[cfg(test)]
11213mod fase35m_mutate_purge_where_tests {
11214    use super::*;
11215
11216    fn parse(src: &str) -> Program {
11217        let tokens = crate::lexer::Lexer::new(src, "<test>")
11218            .tokenize()
11219            .expect("lex");
11220        Parser::new(tokens).parse().expect("parse")
11221    }
11222
11223    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
11224        for d in &prog.declarations {
11225            if let Declaration::Flow(f) = d {
11226                if f.name == flow {
11227                    return f.body.first().expect("flow has at least one step");
11228                }
11229            }
11230        }
11231        panic!("flow `{flow}` not found");
11232    }
11233
11234    #[test]
11235    fn mutate_captures_its_where_clause() {
11236        // Pre-35.m the `{ where: }` block was skipped — every mutate
11237        // ran whole-store. It must now reach `where_expr`.
11238        let prog =
11239            parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
11240        match first_step(&prog, "F") {
11241            FlowStep::Mutate(m) => {
11242                assert_eq!(m.store_name, "accounts");
11243                assert_eq!(m.where_expr, "id = 1");
11244            }
11245            other => panic!("expected Mutate, got {other:?}"),
11246        }
11247    }
11248
11249    #[test]
11250    fn purge_captures_its_where_clause() {
11251        let prog =
11252            parse("flow F() -> Unit { purge logs { where: \"ts < 100\" } }");
11253        match first_step(&prog, "F") {
11254            FlowStep::Purge(p) => {
11255                assert_eq!(p.store_name, "logs");
11256                assert_eq!(p.where_expr, "ts < 100");
11257            }
11258            other => panic!("expected Purge, got {other:?}"),
11259        }
11260    }
11261
11262    #[test]
11263    fn mutate_without_a_where_block_is_a_whole_store_op() {
11264        // No `{ where: }` → an empty filter → the runtime renders
11265        // `WHERE TRUE` (every row). A valid, intentional op.
11266        let prog = parse("flow F() -> Unit { mutate accounts }");
11267        match first_step(&prog, "F") {
11268            FlowStep::Mutate(m) => {
11269                assert_eq!(m.store_name, "accounts");
11270                assert_eq!(m.where_expr, "");
11271            }
11272            other => panic!("expected Mutate, got {other:?}"),
11273        }
11274    }
11275}
11276
11277// ── §Fase 35.o — persist field-block capture ────────────────────────
11278
11279#[cfg(test)]
11280mod fase35o_persist_fields_tests {
11281    use super::*;
11282
11283    fn parse(src: &str) -> Program {
11284        let tokens = crate::lexer::Lexer::new(src, "<test>")
11285            .tokenize()
11286            .expect("lex");
11287        Parser::new(tokens).parse().expect("parse")
11288    }
11289
11290    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
11291        for d in &prog.declarations {
11292            if let Declaration::Flow(f) = d {
11293                if f.name == flow {
11294                    return f.body.first().expect("flow has at least one step");
11295                }
11296            }
11297        }
11298        panic!("flow `{flow}` not found");
11299    }
11300
11301    #[test]
11302    fn persist_captures_its_field_block() {
11303        // Pre-35.o the `{ col: value }` block was skipped — every
11304        // persist wrote the whole binding context. It must now reach
11305        // `fields`, in source order, with value expressions raw.
11306        let prog = parse(
11307            "flow F() -> Unit { persist into chat_history { \
11308             session_id: \"${session_id}\" sender: \"user\" \
11309             content: \"${message}\" } }",
11310        );
11311        match first_step(&prog, "F") {
11312            FlowStep::Persist(p) => {
11313                assert_eq!(p.store_name, "chat_history");
11314                assert_eq!(
11315                    p.fields,
11316                    vec![
11317                        ("session_id".to_string(), "${session_id}".to_string()),
11318                        ("sender".to_string(), "user".to_string()),
11319                        ("content".to_string(), "${message}".to_string()),
11320                    ]
11321                );
11322            }
11323            other => panic!("expected Persist, got {other:?}"),
11324        }
11325    }
11326
11327    #[test]
11328    fn persist_without_a_block_keeps_the_user_bindings_fallback() {
11329        // No `{ }` → empty `fields` → the runtime falls back to the
11330        // v1.30.0 user-bindings row. Backward-compatible.
11331        let prog = parse("flow F() -> Unit { persist events }");
11332        match first_step(&prog, "F") {
11333            FlowStep::Persist(p) => {
11334                assert_eq!(p.store_name, "events");
11335                assert!(p.fields.is_empty());
11336            }
11337            other => panic!("expected Persist, got {other:?}"),
11338        }
11339    }
11340
11341    #[test]
11342    fn persist_accepts_the_optional_into_connector() {
11343        // `persist into X` and `persist X` resolve to the SAME store
11344        // name — pre-35.o `into` was captured AS the store name.
11345        let with =
11346            parse("flow F() -> Unit { persist into accounts { id: \"1\" } }");
11347        let without =
11348            parse("flow F() -> Unit { persist accounts { id: \"1\" } }");
11349        for prog in [&with, &without] {
11350            match first_step(prog, "F") {
11351                FlowStep::Persist(p) => assert_eq!(p.store_name, "accounts"),
11352                other => panic!("expected Persist, got {other:?}"),
11353            }
11354        }
11355    }
11356
11357    #[test]
11358    fn persist_into_without_a_block_resolves_the_store_name() {
11359        // `persist into events` — the `into` connector is skipped, the
11360        // store name is `events` (not `into`). Lateral bug closed.
11361        let prog = parse("flow F() -> Unit { persist into events }");
11362        match first_step(&prog, "F") {
11363            FlowStep::Persist(p) => {
11364                assert_eq!(p.store_name, "events");
11365                assert!(p.fields.is_empty());
11366            }
11367            other => panic!("expected Persist, got {other:?}"),
11368        }
11369    }
11370
11371    #[test]
11372    fn persist_fields_lower_into_the_ir() {
11373        // The IR generator must carry `fields` onto `IRPersistStep`
11374        // so the runtime reads exactly the declared columns.
11375        let prog = parse(
11376            "flow F() -> Unit { persist into chat { content: \"${msg}\" } }",
11377        );
11378        let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
11379        let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
11380        match flow.steps.first().expect("one step") {
11381            crate::ir_nodes::IRFlowNode::Persist(p) => {
11382                assert_eq!(p.store_name, "chat");
11383                assert_eq!(
11384                    p.fields,
11385                    vec![("content".to_string(), "${msg}".to_string())]
11386                );
11387            }
11388            other => panic!("expected IRFlowNode::Persist, got {other:?}"),
11389        }
11390    }
11391}
11392
11393// ── §Fase 35.p — mutate SET-field-block capture ─────────────────────
11394
11395#[cfg(test)]
11396mod fase35p_mutate_fields_tests {
11397    use super::*;
11398
11399    fn parse(src: &str) -> Program {
11400        let tokens = crate::lexer::Lexer::new(src, "<test>")
11401            .tokenize()
11402            .expect("lex");
11403        Parser::new(tokens).parse().expect("parse")
11404    }
11405
11406    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
11407        for d in &prog.declarations {
11408            if let Declaration::Flow(f) = d {
11409                if f.name == flow {
11410                    return f.body.first().expect("flow has at least one step");
11411                }
11412            }
11413        }
11414        panic!("flow `{flow}` not found");
11415    }
11416
11417    #[test]
11418    fn mutate_captures_its_set_field_block() {
11419        // Pre-35.p every key but `where:` was skipped — the runtime
11420        // SET every flow binding. The SET columns must now reach
11421        // `fields`, in source order, with `where:` still captured.
11422        let prog = parse(
11423            "flow F() -> Unit { mutate accounts { where: \"id = ${id}\" \
11424             balance: \"${new_balance}\" status: \"active\" } }",
11425        );
11426        match first_step(&prog, "F") {
11427            FlowStep::Mutate(m) => {
11428                assert_eq!(m.store_name, "accounts");
11429                assert_eq!(m.where_expr, "id = ${id}");
11430                assert_eq!(
11431                    m.fields,
11432                    vec![
11433                        ("balance".to_string(), "${new_balance}".to_string()),
11434                        ("status".to_string(), "active".to_string()),
11435                    ]
11436                );
11437            }
11438            other => panic!("expected Mutate, got {other:?}"),
11439        }
11440    }
11441
11442    #[test]
11443    fn mutate_where_only_block_has_no_set_fields() {
11444        // A `{ where: }`-only block → empty `fields` → the runtime
11445        // falls back to the v1.31.0 user-bindings SET.
11446        let prog =
11447            parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
11448        match first_step(&prog, "F") {
11449            FlowStep::Mutate(m) => {
11450                assert_eq!(m.where_expr, "id = 1");
11451                assert!(m.fields.is_empty());
11452            }
11453            other => panic!("expected Mutate, got {other:?}"),
11454        }
11455    }
11456
11457    #[test]
11458    fn mutate_with_no_block_is_a_whole_store_op() {
11459        // No block at all → empty where + empty fields (a whole-store
11460        // UPDATE from user bindings) — unchanged from 35.m.
11461        let prog = parse("flow F() -> Unit { mutate accounts }");
11462        match first_step(&prog, "F") {
11463            FlowStep::Mutate(m) => {
11464                assert_eq!(m.store_name, "accounts");
11465                assert_eq!(m.where_expr, "");
11466                assert!(m.fields.is_empty());
11467            }
11468            other => panic!("expected Mutate, got {other:?}"),
11469        }
11470    }
11471
11472    #[test]
11473    fn mutate_fields_lower_into_the_ir() {
11474        let prog = parse(
11475            "flow F() -> Unit { mutate t { where: \"id = 1\" v: \"${x}\" } }",
11476        );
11477        let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
11478        let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
11479        match flow.steps.first().expect("one step") {
11480            crate::ir_nodes::IRFlowNode::Mutate(m) => {
11481                assert_eq!(m.where_expr, "id = 1");
11482                assert_eq!(
11483                    m.fields,
11484                    vec![("v".to_string(), "${x}".to_string())]
11485                );
11486            }
11487            other => panic!("expected IRFlowNode::Mutate, got {other:?}"),
11488        }
11489    }
11490}
11491