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        // §Fase 114.a — a top-level `budget` carries its comments like any other
54        // declaration.
55        Declaration::Budget(n) => {
56            n.leading_trivia = leading;
57            n.trailing_trivia = trailing;
58        }
59        Declaration::Import(n) => {
60            n.leading_trivia = leading;
61            n.trailing_trivia = trailing;
62        }
63        Declaration::Persona(n) => {
64            n.leading_trivia = leading;
65            n.trailing_trivia = trailing;
66        }
67        Declaration::Context(n) => {
68            n.leading_trivia = leading;
69            n.trailing_trivia = trailing;
70        }
71        Declaration::Anchor(n) => {
72            n.leading_trivia = leading;
73            n.trailing_trivia = trailing;
74        }
75        Declaration::Memory(n) => {
76            n.leading_trivia = leading;
77            n.trailing_trivia = trailing;
78        }
79        Declaration::Tool(n) => {
80            n.leading_trivia = leading;
81            n.trailing_trivia = trailing;
82        }
83        Declaration::Type(n) => {
84            n.leading_trivia = leading;
85            n.trailing_trivia = trailing;
86        }
87        Declaration::Flow(n) => {
88            n.leading_trivia = leading;
89            n.trailing_trivia = trailing;
90        }
91        Declaration::Intent(n) => {
92            n.leading_trivia = leading;
93            n.trailing_trivia = trailing;
94        }
95        Declaration::Run(n) => {
96            n.leading_trivia = leading;
97            n.trailing_trivia = trailing;
98        }
99        Declaration::Epistemic(n) => {
100            n.leading_trivia = leading;
101            n.trailing_trivia = trailing;
102        }
103        Declaration::Let(n) => {
104            n.leading_trivia = leading;
105            n.trailing_trivia = trailing;
106        }
107        Declaration::LambdaData(n) => {
108            n.leading_trivia = leading;
109            n.trailing_trivia = trailing;
110        }
111        Declaration::Agent(n) => {
112            n.leading_trivia = leading;
113            n.trailing_trivia = trailing;
114        }
115        Declaration::Shield(n) => {
116            n.leading_trivia = leading;
117            n.trailing_trivia = trailing;
118        }
119        Declaration::Window(n) => {
120            n.leading_trivia = leading;
121            n.trailing_trivia = trailing;
122        }
123        Declaration::Pix(n) => {
124            n.leading_trivia = leading;
125            n.trailing_trivia = trailing;
126        }
127        Declaration::Ledger(n) => {
128            n.leading_trivia = leading;
129            n.trailing_trivia = trailing;
130        }
131        Declaration::Psyche(n) => {
132            n.leading_trivia = leading;
133            n.trailing_trivia = trailing;
134        }
135        Declaration::Corpus(n) => {
136            n.leading_trivia = leading;
137            n.trailing_trivia = trailing;
138        }
139        Declaration::Dataspace(n) => {
140            n.leading_trivia = leading;
141            n.trailing_trivia = trailing;
142        }
143        Declaration::Ots(n) => {
144            n.leading_trivia = leading;
145            n.trailing_trivia = trailing;
146        }
147        Declaration::Mandate(n) => {
148            n.leading_trivia = leading;
149            n.trailing_trivia = trailing;
150        }
151        Declaration::Compute(n) => {
152            n.leading_trivia = leading;
153            n.trailing_trivia = trailing;
154        }
155        Declaration::Daemon(n) => {
156            n.leading_trivia = leading;
157            n.trailing_trivia = trailing;
158        }
159        Declaration::Extension(n) => {
160            n.leading_trivia = leading;
161            n.trailing_trivia = trailing;
162        }
163        Declaration::AxonStore(n) => {
164            n.leading_trivia = leading;
165            n.trailing_trivia = trailing;
166        }
167        Declaration::AxonEndpoint(n) => {
168            n.leading_trivia = leading;
169            n.trailing_trivia = trailing;
170        }
171        Declaration::Resource(n) => {
172            n.leading_trivia = leading;
173            n.trailing_trivia = trailing;
174        }
175        Declaration::Fabric(n) => {
176            n.leading_trivia = leading;
177            n.trailing_trivia = trailing;
178        }
179        Declaration::Manifest(n) => {
180            n.leading_trivia = leading;
181            n.trailing_trivia = trailing;
182        }
183        Declaration::Observe(n) => {
184            n.leading_trivia = leading;
185            n.trailing_trivia = trailing;
186        }
187        Declaration::Reconcile(n) => {
188            n.leading_trivia = leading;
189            n.trailing_trivia = trailing;
190        }
191        Declaration::Lease(n) => {
192            n.leading_trivia = leading;
193            n.trailing_trivia = trailing;
194        }
195        Declaration::Ensemble(n) => {
196            n.leading_trivia = leading;
197            n.trailing_trivia = trailing;
198        }
199        Declaration::Session(n) => {
200            n.leading_trivia = leading;
201            n.trailing_trivia = trailing;
202        }
203        Declaration::Topology(n) => {
204            n.leading_trivia = leading;
205            n.trailing_trivia = trailing;
206        }
207        Declaration::Immune(n) => {
208            n.leading_trivia = leading;
209            n.trailing_trivia = trailing;
210        }
211        Declaration::Reflex(n) => {
212            n.leading_trivia = leading;
213            n.trailing_trivia = trailing;
214        }
215        Declaration::Heal(n) => {
216            n.leading_trivia = leading;
217            n.trailing_trivia = trailing;
218        }
219        Declaration::Component(n) => {
220            n.leading_trivia = leading;
221            n.trailing_trivia = trailing;
222        }
223        Declaration::View(n) => {
224            n.leading_trivia = leading;
225            n.trailing_trivia = trailing;
226        }
227        Declaration::Channel(n) => {
228            n.leading_trivia = leading;
229            n.trailing_trivia = trailing;
230        }
231        Declaration::Socket(n) => {
232            n.leading_trivia = leading;
233            n.trailing_trivia = trailing;
234        }
235        Declaration::Upstream(n) => {
236            n.leading_trivia = leading;
237            n.trailing_trivia = trailing;
238        }
239        Declaration::Voice(n) => {
240            n.leading_trivia = leading;
241            n.trailing_trivia = trailing;
242        }
243        Declaration::Cors(n) => {
244            n.leading_trivia = leading;
245            n.trailing_trivia = trailing;
246        }
247        Declaration::Credential(n) => {
248            n.leading_trivia = leading;
249            n.trailing_trivia = trailing;
250        }
251        Declaration::Cache(n) => {
252            n.leading_trivia = leading;
253            n.trailing_trivia = trailing;
254        }
255        Declaration::Savant(n) => {
256            n.leading_trivia = leading;
257            n.trailing_trivia = trailing;
258        }
259        Declaration::Synth(n) => {
260            n.leading_trivia = leading;
261            n.trailing_trivia = trailing;
262        }
263        Declaration::Scope(n) => {
264            n.leading_trivia = leading;
265            n.trailing_trivia = trailing;
266        }
267        Declaration::Observable(n) => {
268            n.leading_trivia = leading;
269            n.trailing_trivia = trailing;
270        }
271        Declaration::Witness(n) => {
272            n.leading_trivia = leading;
273            n.trailing_trivia = trailing;
274        }
275        Declaration::Document(n) => {
276            n.leading_trivia = leading;
277            n.trailing_trivia = trailing;
278        }
279        Declaration::Deliver(n) => {
280            n.leading_trivia = leading;
281            n.trailing_trivia = trailing;
282        }
283        Declaration::Notify(n) => {
284            n.leading_trivia = leading;
285            n.trailing_trivia = trailing;
286        }
287        Declaration::Generic(n) => {
288            n.leading_trivia = leading;
289            n.trailing_trivia = trailing;
290        }
291    }
292}
293
294// ── Public error type ────────────────────────────────────────────────────────
295
296/// §Fase 28.d — Source-context constants. D4 ratified 2026-05-10:
297/// 2 lines before + 2 lines after the error line. Mirror of the
298/// Python-side `_SOURCE_CONTEXT_LINES_BEFORE` / `_AFTER` so the
299/// rustc-style block has identical shape across stacks.
300pub const SOURCE_CONTEXT_LINES_BEFORE: usize = 2;
301pub const SOURCE_CONTEXT_LINES_AFTER: usize = 2;
302
303/// §Fase 28.d — Rustc-style source-context block for a parse error.
304///
305/// Holds a reference to the source text plus the line/column the
306/// error points at. Rendering is lazy — call ``render()`` to format
307/// the block (line numbers + caret + 2 lines before + 2 after).
308///
309/// Pure and deterministic: no ANSI colors, no terminal-width
310/// detection. Output shape is byte-identical to the Python
311/// `SourceSnippet.render()` on the same input — that's the cross-
312/// stack drift gate (28.i).
313#[derive(Debug, Clone)]
314pub struct SourceSnippet {
315    pub source: String,
316    pub line: u32,
317    pub column: u32,
318    pub filename: String,
319    pub context_before: usize,
320    pub context_after: usize,
321}
322
323impl SourceSnippet {
324    /// Construct with the default 2/2 context window.
325    pub fn new(source: String, line: u32, column: u32, filename: String) -> Self {
326        Self {
327            source,
328            line,
329            column,
330            filename,
331            context_before: SOURCE_CONTEXT_LINES_BEFORE,
332            context_after: SOURCE_CONTEXT_LINES_AFTER,
333        }
334    }
335
336    /// Format the snippet as a multi-line rustc-style block.
337    ///
338    /// Empty source → empty string. Out-of-range line → empty
339    /// string. Caret column is clamped to `[1, line_len + 1]`.
340    /// Output shape matches Python `SourceSnippet.render` byte-
341    /// identically per D7.
342    #[must_use]
343    pub fn render(&self) -> String {
344        if self.source.is_empty() || self.line < 1 {
345            return String::new();
346        }
347        let raw: Vec<&str> = self.source.split('\n').collect();
348        // Match Python's str.splitlines() trailing-newline shape:
349        // strip an empty trailing entry produced by a final '\n'.
350        let lines: Vec<&str> = if raw.last() == Some(&"") {
351            raw[..raw.len() - 1].to_vec()
352        } else {
353            raw
354        };
355        if lines.is_empty() || self.line as usize > lines.len() {
356            return String::new();
357        }
358
359        let line_idx = self.line as usize;
360        let start = line_idx.saturating_sub(self.context_before).max(1);
361        let end = (line_idx + self.context_after).min(lines.len());
362
363        let gutter = end.to_string().len();
364        let empty_gutter = " ".repeat(gutter);
365
366        let mut out: Vec<String> = Vec::with_capacity(end - start + 4);
367        out.push(format!(
368            "{empty_gutter} --> {}:{}:{}",
369            self.filename, self.line, self.column
370        ));
371        out.push(format!("{empty_gutter} |"));
372        for n in start..=end {
373            let line_text = lines[n - 1];
374            out.push(format!("{n:>gutter$} | {line_text}", gutter = gutter));
375            if n == line_idx {
376                let line_len = line_text.chars().count();
377                let col = (self.column as usize).clamp(1, line_len + 1);
378                out.push(format!(
379                    "{empty_gutter} | {pad}^",
380                    pad = " ".repeat(col - 1)
381                ));
382            }
383        }
384        out.join("\n")
385    }
386}
387
388#[derive(Debug, Clone, Default)]
389pub struct ParseError {
390    pub message: String,
391    pub line: u32,
392    pub column: u32,
393    /// §Fase 28.d — Optional rustc-style source-context block.
394    /// `None` preserves the legacy single-line shape; populated by
395    /// `Parser::with_source` callers (and by `parse_with_recovery`
396    /// / `parse` when a source has been attached to the parser).
397    /// Existing struct-literal call sites use the `..Default::default()`
398    /// idiom (default = None) to stay terse.
399    pub source_snippet: Option<SourceSnippet>,
400}
401
402impl ParseError {
403    /// §Fase 28.d — Attach a `SourceSnippet` derived from raw source
404    /// text and filename. Returns `self` so the call can be chained
405    /// at the construction site. No-op when `line == 0`. Idempotent.
406    #[must_use]
407    pub fn attach_source(mut self, source: &str, filename: &str) -> Self {
408        if self.line >= 1 {
409            self.source_snippet = Some(SourceSnippet::new(
410                source.to_string(),
411                self.line,
412                self.column,
413                filename.to_string(),
414            ));
415        }
416        self
417    }
418}
419
420impl std::fmt::Display for ParseError {
421    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
422        write!(f, "[line {}:{}] {}", self.line, self.column, self.message)?;
423        if let Some(snippet) = &self.source_snippet {
424            let block = snippet.render();
425            if !block.is_empty() {
426                write!(f, "\n{block}")?;
427            }
428        }
429        Ok(())
430    }
431}
432
433impl std::error::Error for ParseError {}
434
435// ── §Fase 28.c — Public recovery result ──────────────────────────────────────
436//
437// Mirror of Python's `axon.compiler.parser.ParseResult` (Fase 28.b).
438// The rationale, sync semantics, and test contract are documented in
439// `docs/fase/fase_28_adopter_diagnostic_robustness.md`. The Rust frontend
440// must produce structurally identical error lists to the Python parser
441// when handed the same source — that is the cross-stack drift gate
442// (D7 ratified 2026-05-10: byte-identical error lists).
443//
444// `program` holds whatever declarations the parser was able to parse
445// successfully. `errors` holds every recovered error in source order.
446// A clean parse returns `errors.is_empty()`; the existing fail-fast
447// `parse()` API is preserved verbatim per D9.
448
449/// Outcome of `Parser::parse_with_recovery` — partial program plus the
450/// list of every error the parser recovered from. See module docs for
451/// the panic-mode + sync-point recovery semantics.
452#[derive(Debug)]
453pub struct ParseResult {
454    pub program: Program,
455    pub errors: Vec<ParseError>,
456}
457
458impl ParseResult {
459    /// True iff at least one parse error was recovered. Callers that
460    /// want to short-circuit on failure should check this rather than
461    /// relying on `program.declarations.is_empty()` (the parser may
462    /// have salvaged some declarations even with errors present).
463    #[inline]
464    #[must_use]
465    pub fn has_errors(&self) -> bool {
466        !self.errors.is_empty()
467    }
468
469    /// Inverse of `has_errors`. Convenience for the "happy path" check
470    /// in tests + adopter integrations.
471    #[inline]
472    #[must_use]
473    pub fn is_clean(&self) -> bool {
474        self.errors.is_empty()
475    }
476}
477
478/// §Fase 28.c — Top-level declaration keywords used as resync points
479/// during error recovery (D2 ratified 2026-05-10). Mirrors the
480/// `_TOP_LEVEL_DECLARATION_KEYWORDS` frozenset on the Python side.
481///
482/// Distinct from `tokens::is_declaration_keyword` because that helper
483/// is used by the structural declaration counter and intentionally
484/// excludes some grammar-only tokens (Know/Believe/Speculate/Doubt,
485/// Ingest, Ots) that DO begin a top-level declaration in
486/// `parse_declaration` and therefore must be valid sync points.
487///
488/// Adding a new top-level dispatch arm in `parse_declaration` MUST
489/// add the corresponding token here so the recovery walker can
490/// re-sync correctly.
491#[inline]
492const fn is_top_level_decl_kw_for_recovery(tt: &TokenType) -> bool {
493    matches!(
494        tt,
495        TokenType::Import
496            | TokenType::Persona
497            | TokenType::Context
498            | TokenType::Anchor
499            | TokenType::Memory
500            | TokenType::Tool
501            | TokenType::Type
502            | TokenType::Flow
503            | TokenType::Intent
504            | TokenType::Run
505            | TokenType::Let
506            | TokenType::Know
507            | TokenType::Believe
508            | TokenType::Speculate
509            | TokenType::Doubt
510            | TokenType::Lambda
511            | TokenType::Agent
512            | TokenType::Shield
513            | TokenType::Pix
514            | TokenType::Ledger
515            | TokenType::Psyche
516            | TokenType::Corpus
517            | TokenType::Dataspace
518            | TokenType::Ots
519            | TokenType::Mandate
520            | TokenType::Compute
521            | TokenType::Daemon
522            // §Fase 87.a/d — the autonomous research primitive + synth policy.
523            | TokenType::Savant
524            | TokenType::Synth
525            // §Fase 88.a — the authorization-scope policy declaration.
526            | TokenType::Scope
527            | TokenType::AxonStore
528            | TokenType::AxonEndpoint
529            | TokenType::Resource
530            | TokenType::Fabric
531            | TokenType::Manifest
532            | TokenType::Observe
533            | TokenType::Reconcile
534            | TokenType::Lease
535            | TokenType::Ensemble
536            | TokenType::Session
537            | TokenType::Topology
538            | TokenType::Immune
539            | TokenType::Reflex
540            | TokenType::Heal
541            | TokenType::Component
542            | TokenType::View
543            | TokenType::Channel
544            | TokenType::Ingest
545            | TokenType::Persist
546            | TokenType::Retrieve
547            | TokenType::Mutate
548            | TokenType::Purge
549            | TokenType::Transact
550            | TokenType::Mcp
551    )
552}
553
554// ── §Fase 30.b — axonendpoint transport + keepalive closed enums ────────────
555//
556// D2 ratified 2026-05-10: `transport` is a closed enum
557// {json, sse, ndjson}. D6 ratified: `keepalive` is a closed enum
558// {5s, 15s, 30s, 60s}. Both mirror the Python frontend's
559// `_AXONENDPOINT_TRANSPORT_VALUES` / `_AXONENDPOINT_KEEPALIVE_VALUES`
560// frozensets in `axon/compiler/parser.py`. Cross-stack drift gate
561// (30.b fixture) asserts byte-identical parse for every entry.
562
563/// Adopter-facing acceptable values for `transport:` field.
564/// Used by both the parser (validation + smart-suggest) and the
565/// type-checker (30.c) so adopter tooling sees one canonical list.
566pub const AXONENDPOINT_TRANSPORT_VALUES: &[&str] = &["json", "sse", "ndjson"];
567
568/// §Fase 33.z.k.b (v1.28.0) — Closed-catalog SSE wire-format
569/// dialects. Selected via the parametrized grammar
570/// `transport: sse(<dialect>)`; bare `transport: sse` resolves to
571/// the Q1 default per the flow's algebraic-effect predicate
572/// (openai for tool-streaming flows; axon for type-annotation-only).
573///
574/// Vertical-grounded scope (Q3 revised 2026-05-14): five dialects
575/// cover ~99% of LLM-streaming adopter expectations.
576///   - `axon`      — current W3C named events
577///                   (event: axon.token / event: axon.complete).
578///                   D6 backwards-compat baseline; indefinitely
579///                   supported as a first-class option.
580///   - `openai`    — `data: {"choices":[{"delta":{...}}]}` frames
581///                   terminated by `data: [DONE]`. OpenAI Chat
582///                   Completions streaming wire verbatim.
583///   - `kimi`      — Moonshot Kimi (kimi.moonshot.cn) — uses the
584///                   OpenAI-compatible Chat Completions wire format
585///                   verbatim (same chunk shape, same `data: [DONE]`
586///                   sentinel). First-class entry so adopters
587///                   declare intent explicitly; under the hood the
588///                   wire is identical to `openai`.
589///   - `glm`       — Zhipu ChatGLM (open.bigmodel.cn) — same as
590///                   kimi, uses OpenAI-compat wire. First-class
591///                   entry for adopter clarity.
592///   - `anthropic` — `event: content_block_delta` frames terminated
593///                   by `event: message_stop`. Adopter SDKs
594///                   targeting Anthropic Claude consume this shape
595///                   verbatim.
596///
597/// Why kimi + glm as first-class entries (Q3 revision rationale):
598/// The project's primary adopter pipelines through Kimi K2.x +
599/// Zhipu GLM-4.x. While the wire IS byte-identical to OpenAI's
600/// Chat Completions streaming, declaring `transport: sse(kimi)` /
601/// `transport: sse(glm)` lets the audit trail + observability
602/// surfaces correlate adopter intent against the underlying
603/// provider — without the adopter having to know that "kimi
604/// happens to be OpenAI-compat on the wire today". The runtime
605/// dispatches kimi + glm to the same `OpenAIDialectAdapter` so
606/// the wire shape stays canonical-OpenAI-bytes.
607///
608/// Open-set adapter pluggability (downstream crates registering
609/// custom dialects) remains explicitly out of scope per the
610/// Axon-for-Axon discipline.
611pub const AXONENDPOINT_TRANSPORT_DIALECTS: &[&str] =
612    &["axon", "openai", "kimi", "glm", "anthropic"];
613
614/// Adopter-facing acceptable values for `keepalive:` field.
615pub const AXONENDPOINT_KEEPALIVE_VALUES: &[&str] = &["5s", "15s", "30s", "60s"];
616
617/// §Fase 32.b D3 — Closed method enum for `method:` field. Adopter-
618/// declarable methods only; HEAD/OPTIONS/CONNECT/TRACE are
619/// runtime-managed (CORS preflight, etc.) and never declared from
620/// source. Closed enum refuses interpretation drift; smart-suggest
621/// catches near-misses at parse time.
622///
623/// §Fase 107.a — `QUERY` (RFC 10008, Proposed Standard, June 2026): the safe +
624/// idempotent + cacheable method that CARRIES A REQUEST BODY — the first new HTTP
625/// method in two decades. It carries a LAW, not just a route: `axon-T927` refuses
626/// at compile time a QUERY endpoint whose flow performs a declared write (the
627/// RFC's normative "safe and idempotent" MUST, made a proof).
628///
629/// Must stay in lockstep with `type_checker::VALID_ENDPOINT_METHODS`.
630pub const AXONENDPOINT_METHOD_VALUES: &[&str] =
631    &["GET", "POST", "PUT", "DELETE", "PATCH", "QUERY"];
632
633/// §Fase 36.d (D2) — Closed catalog for the `axonendpoint backend:`
634/// declaration. The set is `CANONICAL_PROVIDERS ∪ {auto, stub}`:
635///
636///   - the seven canonical LLM providers — `anthropic`, `gemini`,
637///     `glm`, `kimi`, `ollama`, `openai`, `openrouter` — a concrete,
638///     declared backend that rung 2 of the Fase 36 D1 resolution
639///     ladder fires immediately;
640///   - `auto` — transparent: declaring it is equivalent to omitting
641///     `backend:` entirely (the route resolves down the ladder —
642///     server default → environment-available providers);
643///   - `stub` — the no-op backend, reachable ONLY by an explicit,
644///     written declaration (D5: a silent degradation to `stub` is
645///     forbidden; an explicit opt-in is not).
646///
647/// `axon-frontend` carries zero runtime deps and therefore cannot
648/// import `axon::backends::CANONICAL_PROVIDERS`; this list is a
649/// hand-maintained mirror. The axon-rs drift gate
650/// (`tests/fase36_d_backend_catalog_drift.rs`) asserts the two stay
651/// byte-identical — adding a provider in one place without the other
652/// fails CI.
653pub const AXONENDPOINT_BACKEND_VALUES: &[&str] = &[
654    "anthropic",
655    "auto",
656    "gemini",
657    "glm",
658    "kimi",
659    "ollama",
660    "openai",
661    "openrouter",
662    "stub",
663];
664
665#[inline]
666fn axonendpoint_is_valid_transport(s: &str) -> bool {
667    AXONENDPOINT_TRANSPORT_VALUES.iter().any(|&v| v == s)
668}
669
670#[inline]
671fn axonendpoint_is_valid_method(s: &str) -> bool {
672    AXONENDPOINT_METHOD_VALUES.iter().any(|&v| v == s)
673}
674
675#[inline]
676fn axonendpoint_is_valid_backend(s: &str) -> bool {
677    AXONENDPOINT_BACKEND_VALUES.iter().any(|&v| v == s)
678}
679
680#[inline]
681fn axonendpoint_is_valid_keepalive(s: &str) -> bool {
682    AXONENDPOINT_KEEPALIVE_VALUES.iter().any(|&v| v == s)
683}
684
685/// §Fase 37.y (D2) — Closed type catalog for query parameters.
686///
687/// Query values arrive over HTTP as URL-encoded strings; the catalog
688/// is the set of types axon will validate / coerce them into for the
689/// Request Binding Contract. Hand-curated, intentionally small:
690///   - `Text` — the raw string (always succeeds)
691///   - `Int` — `i64` parseable
692///   - `Float` — `f64` parseable, finite
693///   - `Bool` — case-insensitive `{true, false, 1, 0, yes, no, on, off}`
694///   - `Uuid` — RFC 4122 textual form
695///
696/// Extending the catalog is a future axon-T?nn surface; v1.38.5 ships
697/// the 5 types covering ~95% of REST query patterns. Lists / dates /
698/// datetimes / enums are honest deferrals (see §7 of the plan vivo).
699pub const AXONENDPOINT_QUERY_PARAM_TYPES: &[&str] =
700    &["Text", "Int", "Float", "Bool", "Uuid"];
701
702/// `true` iff `s` is one of the §Fase 37.y (D2) query-param catalog
703/// entries — exact case-sensitive match (axon types are PascalCase).
704#[inline]
705pub(crate) fn axonendpoint_is_valid_query_param_type(s: &str) -> bool {
706    AXONENDPOINT_QUERY_PARAM_TYPES.iter().any(|&v| v == s)
707}
708
709/// §Fase 37.y (D1) — Extract `{name}` placeholder names from an
710/// `axonendpoint` `path:` string, in left-to-right declaration order.
711///
712/// Recognized placeholder grammar (single-segment, no nested braces):
713/// `{NAME}` where `NAME` matches `[A-Za-z_][A-Za-z0-9_]*`. Anything
714/// inside braces that does NOT match the identifier shape is silently
715/// IGNORED — it's either an adopter typo (caught later by axum at
716/// route registration) or a literal brace in the URL pattern.
717///
718/// Returns `Err(duplicate_name)` when the same `{name}` appears more
719/// than once in the path — HTTP route patterns reject duplicates
720/// structurally (`axum` would panic at registration), so surfacing
721/// the error at parse time is the right place.
722///
723/// Pure + total: never panics; deterministic over its single string
724/// argument. Hand-rolled scanner (no regex dep at parser layer).
725///
726/// # Examples
727///
728/// - `"/api/users"` → `Ok(vec![])`
729/// - `"/api/users/{id}"` → `Ok(vec!["id"])`
730/// - `"/api/tenants/{tenant_id}/secrets/{secret_name}"`
731///   → `Ok(vec!["tenant_id", "secret_name"])`
732/// - `"/api/users/{id}/posts/{id}"` → `Err("id")` (duplicate)
733/// - `"/api/{not valid}"` → `Ok(vec![])` (malformed brace content
734///   silently ignored; axum surfaces the error at registration)
735pub(crate) fn extract_path_param_names(path: &str) -> Result<Vec<String>, String> {
736    let mut out: Vec<String> = Vec::new();
737    let bytes = path.as_bytes();
738    let mut i = 0;
739    while i < bytes.len() {
740        if bytes[i] != b'{' {
741            i += 1;
742            continue;
743        }
744        // Find the matching close brace; if none, the open brace is
745        // a literal — leave it alone.
746        let start = i + 1;
747        let mut end = start;
748        while end < bytes.len() && bytes[end] != b'}' {
749            end += 1;
750        }
751        if end == bytes.len() {
752            // Unterminated — give up; downstream parser/runtime
753            // surface the malformed path elsewhere.
754            break;
755        }
756        let raw = &path[start..end];
757        // Validate identifier shape: [A-Za-z_][A-Za-z0-9_]*
758        let valid = !raw.is_empty()
759            && raw.bytes().enumerate().all(|(idx, b)| {
760                if idx == 0 {
761                    b.is_ascii_alphabetic() || b == b'_'
762                } else {
763                    b.is_ascii_alphanumeric() || b == b'_'
764                }
765            });
766        if valid {
767            let name = raw.to_string();
768            if out.iter().any(|existing| existing == &name) {
769                return Err(name);
770            }
771            out.push(name);
772        }
773        i = end + 1;
774    }
775    Ok(out)
776}
777
778/// §Fase 32.g (D8) — Closed capability-slug grammar. Validates a
779/// `requires:` slug per `^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$`.
780///
781/// Hand-rolled (no regex dep at parser layer) — each segment must
782/// match `[a-z][a-z0-9_]*` and segments are joined by single dots.
783/// Public so the runtime mirror (`axon::auth_scope`) reuses the same
784/// predicate without duplicating the rule.
785///
786/// Examples valid: `admin`, `legal.read`, `hipaa.phi.read`,
787/// `bank.officer.senior`, `a`, `a_b`, `a1`.
788/// Examples invalid: empty, `Admin` (uppercase), `1admin` (digit
789/// first), `bank-officer` (hyphen), `bank..a` (empty segment),
790/// `.admin`, `admin.`, `admin..` .
791pub fn is_valid_capability_slug(slug: &str) -> bool {
792    if slug.is_empty() {
793        return false;
794    }
795    for segment in slug.split('.') {
796        if !is_valid_slug_segment(segment) {
797            return false;
798        }
799    }
800    true
801}
802
803fn is_valid_slug_segment(seg: &str) -> bool {
804    let mut chars = seg.chars();
805    let first = match chars.next() {
806        Some(c) => c,
807        None => return false,
808    };
809    if !first.is_ascii_lowercase() {
810        return false;
811    }
812    chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
813}
814
815// ════════════════════════════════════════════════════════════════════
816//  §Fase 37.y (D1) — `extract_path_param_names` unit tests
817// ════════════════════════════════════════════════════════════════════
818
819// ════════════════════════════════════════════════════════════════════
820//  §Fase 37.y (D2) — `axonendpoint_is_valid_query_param_type` + the
821//  inline `query: { … }` parser, end-to-end through the lexer.
822// ════════════════════════════════════════════════════════════════════
823
824#[cfg(test)]
825mod query_param_catalog_tests {
826    use super::{axonendpoint_is_valid_query_param_type, AXONENDPOINT_QUERY_PARAM_TYPES};
827
828    #[test]
829    fn accepts_every_catalog_entry() {
830        for ty in AXONENDPOINT_QUERY_PARAM_TYPES {
831            assert!(
832                axonendpoint_is_valid_query_param_type(ty),
833                "catalog entry `{ty}` must validate"
834            );
835        }
836    }
837
838    #[test]
839    fn rejects_off_catalog_types() {
840        for off in &[
841            "Timestamp",    // not in v1.38.5 — list/dates deferred
842            "Date",
843            "DateTime",
844            "List<Text>",   // multi-value query params deferred (§7)
845            "Jsonb",        // store-only types not query-applicable
846            "Bytea",
847            "text",         // lowercase rejected (axon types are PascalCase)
848            "TEXT",
849            "Number",       // not in axon's type catalog at all
850            "",             // empty
851            " ",            // whitespace
852        ] {
853            assert!(
854                !axonendpoint_is_valid_query_param_type(off),
855                "off-catalog `{off}` must reject"
856            );
857        }
858    }
859
860    #[test]
861    fn catalog_size_matches_design() {
862        // The plan vivo D2 states a closed 5-type catalog. A future
863        // axon-T?nn surface may extend it; that requires updating BOTH
864        // the catalog AND the plan vivo §7 honest-scope note.
865        assert_eq!(AXONENDPOINT_QUERY_PARAM_TYPES.len(), 5);
866    }
867}
868
869#[cfg(test)]
870mod query_param_parser_tests {
871    use crate::lexer::Lexer;
872    use crate::parser::Parser;
873
874    fn parse_endpoint_source(src: &str) -> Result<crate::ast::AxonEndpointDefinition, String> {
875        let tokens = Lexer::new(src, "test.axon")
876            .tokenize()
877            .map_err(|e| format!("lex: {}", e.message))?;
878        let mut parser = Parser::new(tokens);
879        let program = parser.parse().map_err(|e| format!("parse: {}", e.message))?;
880        program
881            .declarations
882            .into_iter()
883            .find_map(|d| match d {
884                crate::ast::Declaration::AxonEndpoint(e) => Some(e),
885                _ => None,
886            })
887            .ok_or_else(|| "no axonendpoint in program".to_string())
888    }
889
890    #[test]
891    fn endpoint_with_no_query_block_keeps_empty_vec() {
892        let src = r#"
893            axonendpoint write_secret {
894                method: POST
895                path: "/api/users"
896                body: SecretWriteRequest
897                execute: WriteSecret
898            }
899        "#;
900        let ep = parse_endpoint_source(src).expect("parses");
901        assert!(
902            ep.query_params.is_empty(),
903            "D5 — no `query:` block ⇒ empty query_params"
904        );
905    }
906
907    #[test]
908    fn single_query_param_required() {
909        let src = r#"
910            axonendpoint list_users {
911                method: GET
912                path: "/api/users"
913                query: { status: Text }
914                execute: ListUsers
915            }
916        "#;
917        let ep = parse_endpoint_source(src).expect("parses");
918        assert_eq!(ep.query_params.len(), 1);
919        assert_eq!(ep.query_params[0].name, "status");
920        assert_eq!(ep.query_params[0].type_expr.name, "Text");
921        assert!(!ep.query_params[0].type_expr.optional);
922    }
923
924    #[test]
925    fn optional_query_param_via_question_suffix() {
926        let src = r#"
927            axonendpoint list_users {
928                method: GET
929                path: "/api/users"
930                query: { limit: Int? }
931                execute: ListUsers
932            }
933        "#;
934        let ep = parse_endpoint_source(src).expect("parses");
935        assert_eq!(ep.query_params.len(), 1);
936        assert_eq!(ep.query_params[0].name, "limit");
937        assert_eq!(ep.query_params[0].type_expr.name, "Int");
938        assert!(
939            ep.query_params[0].type_expr.optional,
940            "`?` suffix sets optional"
941        );
942    }
943
944    #[test]
945    fn multiple_query_params_preserve_declaration_order() {
946        let src = r#"
947            axonendpoint search {
948                method: GET
949                path: "/api/search"
950                query: { q: Text, page: Int?, limit: Int?, exact: Bool? }
951                execute: Search
952            }
953        "#;
954        let ep = parse_endpoint_source(src).expect("parses");
955        let names: Vec<&str> = ep.query_params.iter().map(|f| f.name.as_str()).collect();
956        assert_eq!(names, vec!["q", "page", "limit", "exact"]);
957        let types: Vec<&str> = ep
958            .query_params
959            .iter()
960            .map(|f| f.type_expr.name.as_str())
961            .collect();
962        assert_eq!(types, vec!["Text", "Int", "Int", "Bool"]);
963        let optionals: Vec<bool> = ep
964            .query_params
965            .iter()
966            .map(|f| f.type_expr.optional)
967            .collect();
968        assert_eq!(optionals, vec![false, true, true, true]);
969    }
970
971    #[test]
972    fn duplicate_query_param_is_parse_error() {
973        let src = r#"
974            axonendpoint bad {
975                method: GET
976                path: "/api/x"
977                query: { name: Text, name: Int? }
978                execute: Bad
979            }
980        "#;
981        let err = parse_endpoint_source(src).expect_err("must fail");
982        assert!(
983            err.contains("duplicate query param 'name'"),
984            "error must name the duplicate. Got: {err}"
985        );
986    }
987
988    #[test]
989    fn off_catalog_type_with_smart_suggest_hint() {
990        // `Strng` is one edit away from `Text` (would suggest `Text`?
991        // Actually edit distance to `Text` is 4; to `Int` is 5. Likely
992        // no smart suggestion within distance 2. The error still names
993        // the catalog explicitly.)
994        let src = r#"
995            axonendpoint bad {
996                method: GET
997                path: "/api/x"
998                query: { value: Strng }
999                execute: Bad
1000            }
1001        "#;
1002        let err = parse_endpoint_source(src).expect_err("must fail");
1003        assert!(
1004            err.contains("unsupported type 'Strng'"),
1005            "error must name the bad type. Got: {err}"
1006        );
1007        assert!(
1008            err.contains("Expected one of: Text | Int | Float | Bool | Uuid"),
1009            "error must list the closed catalog. Got: {err}"
1010        );
1011    }
1012
1013    #[test]
1014    fn close_typo_gets_did_you_mean_hint() {
1015        // `Txt` → edit distance 1 from `Text` → smart-suggest should
1016        // surface the hint.
1017        let src = r#"
1018            axonendpoint bad {
1019                method: GET
1020                path: "/api/x"
1021                query: { value: Txt }
1022                execute: Bad
1023            }
1024        "#;
1025        let err = parse_endpoint_source(src).expect_err("must fail");
1026        assert!(
1027            err.contains("Did you mean") && err.contains("`Text`"),
1028            "smart-suggest must hint `Text`. Got: {err}"
1029        );
1030    }
1031
1032    #[test]
1033    fn every_catalog_type_parses_cleanly() {
1034        // Round-trip smoke for all 5 catalog entries.
1035        for ty in &["Text", "Int", "Float", "Bool", "Uuid"] {
1036            let src = format!(
1037                r#"
1038                    axonendpoint x {{
1039                        method: GET
1040                        path: "/api/x"
1041                        query: {{ v: {ty} }}
1042                        execute: X
1043                    }}
1044                "#
1045            );
1046            let ep = parse_endpoint_source(&src)
1047                .unwrap_or_else(|e| panic!("`{ty}` should parse: {e}"));
1048            assert_eq!(ep.query_params[0].type_expr.name, *ty);
1049        }
1050    }
1051
1052    #[test]
1053    fn comma_optional_between_params() {
1054        // The plan vivo design accepts both comma-separated and
1055        // whitespace-separated query params (existing parser style is
1056        // forgiving). Whitespace-only:
1057        let src = r#"
1058            axonendpoint x {
1059                method: GET
1060                path: "/api/x"
1061                query: { a: Text b: Int? }
1062                execute: X
1063            }
1064        "#;
1065        let ep = parse_endpoint_source(src).expect("parses without commas");
1066        assert_eq!(ep.query_params.len(), 2);
1067    }
1068
1069    // ─── Robustness hardening (37.y.2 100% robust closure) ──────────
1070
1071    #[test]
1072    fn double_query_block_is_parse_error() {
1073        // An adopter who copy-pastes the `query:` block twice should
1074        // see a clear parse error, not a silent merge that produces
1075        // an unexpectedly-augmented endpoint with both blocks fused.
1076        let src = r#"
1077            axonendpoint x {
1078                method: GET
1079                path: "/api/x"
1080                query: { a: Text }
1081                query: { b: Int? }
1082                execute: X
1083            }
1084        "#;
1085        let err = parse_endpoint_source(src).expect_err("must fail");
1086        assert!(
1087            err.contains("declares `query: { … }` more than once"),
1088            "error must call out the duplicate block. Got: {err}"
1089        );
1090        assert!(
1091            err.contains("combine all params into a single block"),
1092            "error must hint the canonical fix. Got: {err}"
1093        );
1094    }
1095
1096    #[test]
1097    fn optional_generic_type_is_parse_error_with_canonical_hint() {
1098        // `Optional<Text>` is the wrong way to declare an optional
1099        // query param. The canonical syntax is `Text?` (the `?`
1100        // suffix). The error must surface this with a literal example.
1101        let src = r#"
1102            axonendpoint x {
1103                method: GET
1104                path: "/api/x"
1105                query: { value: Optional<Text> }
1106                execute: X
1107            }
1108        "#;
1109        let err = parse_endpoint_source(src).expect_err("must fail");
1110        assert!(
1111            err.contains("generic type `Optional<Text>`"),
1112            "error must name the generic type literally. Got: {err}"
1113        );
1114        assert!(
1115            err.contains("Use `Text?` (the `?` suffix)"),
1116            "error must hint the canonical `Text?` syntax. Got: {err}"
1117        );
1118    }
1119
1120    #[test]
1121    fn list_generic_type_is_parse_error_with_deferral_hint() {
1122        // Multi-value query params (`?tag=a&tag=b`) are honest-
1123        // deferred per the plan vivo §7. Adopters who write
1124        // `List<Text>` should see a clear error explaining the
1125        // deferral, not a confusing "type `List` not in catalog".
1126        let src = r#"
1127            axonendpoint x {
1128                method: GET
1129                path: "/api/x"
1130                query: { tags: List<Text> }
1131                execute: X
1132            }
1133        "#;
1134        let err = parse_endpoint_source(src).expect_err("must fail");
1135        assert!(
1136            err.contains("generic type `List<Text>`"),
1137            "error must name the generic type. Got: {err}"
1138        );
1139        assert!(
1140            err.contains("Multi-value query params")
1141                && err.contains("honest-deferred"),
1142            "error must mention the multi-value deferral. Got: {err}"
1143        );
1144    }
1145
1146    #[test]
1147    fn other_generic_types_caught_generically() {
1148        // Generic types beyond `Optional` and `List` get the
1149        // generic-rejection message without a canonical-syntax hint
1150        // (the catalog list is the canonical guidance).
1151        let src = r#"
1152            axonendpoint x {
1153                method: GET
1154                path: "/api/x"
1155                query: { value: Stream<Int> }
1156                execute: X
1157            }
1158        "#;
1159        let err = parse_endpoint_source(src).expect_err("must fail");
1160        assert!(
1161            err.contains("generic type `Stream<Int>`"),
1162            "error must name the generic type. Got: {err}"
1163        );
1164        assert!(
1165            err.contains("Text | Int | Float | Bool | Uuid"),
1166            "error must list the closed catalog. Got: {err}"
1167        );
1168    }
1169
1170    #[test]
1171    fn uuid_optional_parses_cleanly() {
1172        // Hardening companion — `Uuid?` is in the catalog AND
1173        // optional. The two features compose without surprise.
1174        let src = r#"
1175            axonendpoint find {
1176                method: GET
1177                path: "/api/x"
1178                query: { after: Uuid? }
1179                execute: Find
1180            }
1181        "#;
1182        let ep = parse_endpoint_source(src).expect("parses");
1183        assert_eq!(ep.query_params.len(), 1);
1184        assert_eq!(ep.query_params[0].name, "after");
1185        assert_eq!(ep.query_params[0].type_expr.name, "Uuid");
1186        assert!(ep.query_params[0].type_expr.optional);
1187        assert_eq!(ep.query_params[0].type_expr.generic_param, "");
1188    }
1189
1190    #[test]
1191    fn empty_query_block_yields_empty_vec() {
1192        // `query: { }` is grammatically valid but semantically a
1193        // no-op (equivalent to omitting the block). Don't error;
1194        // just record an empty Vec.
1195        let src = r#"
1196            axonendpoint x {
1197                method: GET
1198                path: "/api/x"
1199                query: { }
1200                execute: X
1201            }
1202        "#;
1203        let ep = parse_endpoint_source(src).expect("empty block parses");
1204        assert!(ep.query_params.is_empty());
1205    }
1206
1207    #[test]
1208    fn kivi_secret_write_path_plus_query() {
1209        // Combined path-param + query-param test: an endpoint that
1210        // takes IDs in the URL AND optional filters in the query
1211        // string. This is the natural REST shape Fase 37.y serves.
1212        let src = r#"
1213            axonendpoint write_secret {
1214                method: POST
1215                path: "/api/tenants/{tenant_id}/secrets/{secret_name}"
1216                query: { dry_run: Bool?, overwrite: Bool? }
1217                body: SecretWriteRequest
1218                execute: WriteSecret
1219            }
1220        "#;
1221        let ep = parse_endpoint_source(src).expect("parses");
1222        // Path params populated (from 37.y.1):
1223        assert_eq!(ep.path_params, vec!["tenant_id", "secret_name"]);
1224        // Query params populated (from this sub-fase 37.y.2):
1225        assert_eq!(ep.query_params.len(), 2);
1226        assert_eq!(ep.query_params[0].name, "dry_run");
1227        assert_eq!(ep.query_params[0].type_expr.name, "Bool");
1228        assert!(ep.query_params[0].type_expr.optional);
1229        assert_eq!(ep.query_params[1].name, "overwrite");
1230        // Body still works:
1231        assert_eq!(ep.body_type, "SecretWriteRequest");
1232    }
1233}
1234
1235#[cfg(test)]
1236mod path_param_extraction_tests {
1237    use super::extract_path_param_names;
1238
1239    #[test]
1240    fn empty_path_no_placeholders() {
1241        assert_eq!(extract_path_param_names("/api/users"), Ok(vec![]));
1242        assert_eq!(extract_path_param_names("/"), Ok(vec![]));
1243        assert_eq!(extract_path_param_names(""), Ok(vec![]));
1244    }
1245
1246    #[test]
1247    fn single_placeholder() {
1248        assert_eq!(
1249            extract_path_param_names("/api/users/{id}"),
1250            Ok(vec!["id".to_string()])
1251        );
1252    }
1253
1254    #[test]
1255    fn multiple_placeholders_in_declaration_order() {
1256        assert_eq!(
1257            extract_path_param_names(
1258                "/api/tenants/{tenant_id}/secrets/{secret_name}"
1259            ),
1260            Ok(vec![
1261                "tenant_id".to_string(),
1262                "secret_name".to_string(),
1263            ])
1264        );
1265    }
1266
1267    #[test]
1268    fn kivi_chat_history_path_pattern() {
1269        // The exact pattern the kivi adopter reported (2026-05-20):
1270        // POST /api/tenants/{tenant_id}/secrets/{secret_name}
1271        // Both names extracted in source order.
1272        let names = extract_path_param_names(
1273            "/api/tenants/{tenant_id}/secrets/{secret_name}",
1274        );
1275        assert_eq!(
1276            names,
1277            Ok(vec![
1278                "tenant_id".to_string(),
1279                "secret_name".to_string(),
1280            ])
1281        );
1282    }
1283
1284    #[test]
1285    fn duplicate_placeholder_returns_err() {
1286        assert_eq!(
1287            extract_path_param_names("/api/users/{id}/posts/{id}"),
1288            Err("id".to_string())
1289        );
1290    }
1291
1292    #[test]
1293    fn underscore_and_numeric_in_name() {
1294        assert_eq!(
1295            extract_path_param_names("/api/{user_id}/items/{item_2}"),
1296            Ok(vec!["user_id".to_string(), "item_2".to_string()])
1297        );
1298    }
1299
1300    #[test]
1301    fn leading_underscore_accepted() {
1302        // Identifiers in HTTP paths often start with letters but the
1303        // grammar permits leading underscore (parity with Rust identifier
1304        // rules). The flow parameter name on the binding side has to
1305        // match exactly, so adopters with `_internal_id` in the path
1306        // can pair it with a same-named flow param.
1307        assert_eq!(
1308            extract_path_param_names("/api/{_internal}"),
1309            Ok(vec!["_internal".to_string()])
1310        );
1311    }
1312
1313    #[test]
1314    fn malformed_placeholder_silently_ignored() {
1315        // Content inside `{...}` that does not match the identifier
1316        // grammar is skipped at this layer. axum surfaces the route
1317        // registration failure if the literal text is invalid.
1318        assert_eq!(
1319            extract_path_param_names("/api/{not valid}"),
1320            Ok(vec![])
1321        );
1322        // Empty braces — same: skip silently.
1323        assert_eq!(extract_path_param_names("/api/{}"), Ok(vec![]));
1324        // Mixed: malformed brace skipped, valid placeholder kept.
1325        assert_eq!(
1326            extract_path_param_names("/api/{tenant id}/users/{id}"),
1327            Ok(vec!["id".to_string()])
1328        );
1329    }
1330
1331    #[test]
1332    fn unterminated_brace_returns_clean() {
1333        // Open brace with no close brace — give up without panicking.
1334        // (axum surfaces the malformed-route error at registration.)
1335        assert_eq!(extract_path_param_names("/api/{id"), Ok(vec![]));
1336    }
1337
1338    #[test]
1339    fn placeholders_at_path_boundaries() {
1340        // Placeholder as the very first segment AND the very last
1341        // segment — both should be extracted.
1342        assert_eq!(
1343            extract_path_param_names("{prefix}/api/users/{id}"),
1344            Ok(vec!["prefix".to_string(), "id".to_string()])
1345        );
1346        assert_eq!(
1347            extract_path_param_names("/api/{id}"),
1348            Ok(vec!["id".to_string()])
1349        );
1350    }
1351
1352    #[test]
1353    fn deduplication_detects_non_adjacent_duplicates() {
1354        // The duplicate-detection sweep is global, not just adjacent.
1355        assert_eq!(
1356            extract_path_param_names(
1357                "/api/orgs/{org_id}/teams/{team_id}/repos/{org_id}"
1358            ),
1359            Err("org_id".to_string())
1360        );
1361    }
1362
1363    #[test]
1364    fn never_panics_on_arbitrary_input() {
1365        // Light fuzz: a handful of weird inputs return cleanly.
1366        for input in &[
1367            "{",
1368            "}",
1369            "{}",
1370            "{{}}",
1371            "{{{",
1372            "/api/{}/{id}",
1373            "////",
1374            "\u{1F4A1}",        // emoji (lightbulb)
1375            "\u{0000}",         // null byte
1376        ] {
1377            let _ = extract_path_param_names(input); // must not panic
1378        }
1379    }
1380}
1381
1382#[cfg(test)]
1383mod capability_slug_tests {
1384    use super::is_valid_capability_slug;
1385
1386    #[test]
1387    fn accepts_canonical_examples() {
1388        assert!(is_valid_capability_slug("admin"));
1389        assert!(is_valid_capability_slug("legal.read"));
1390        assert!(is_valid_capability_slug("hipaa.phi.read"));
1391        assert!(is_valid_capability_slug("bank.officer.senior"));
1392        assert!(is_valid_capability_slug("a"));
1393        assert!(is_valid_capability_slug("a_b"));
1394        assert!(is_valid_capability_slug("a1"));
1395        assert!(is_valid_capability_slug("a.b1_c"));
1396    }
1397
1398    #[test]
1399    fn rejects_empty_string() {
1400        assert!(!is_valid_capability_slug(""));
1401    }
1402
1403    #[test]
1404    fn rejects_uppercase() {
1405        assert!(!is_valid_capability_slug("Admin"));
1406        assert!(!is_valid_capability_slug("admin.READ"));
1407    }
1408
1409    #[test]
1410    fn rejects_digit_first() {
1411        assert!(!is_valid_capability_slug("1admin"));
1412        assert!(!is_valid_capability_slug("admin.1read"));
1413    }
1414
1415    #[test]
1416    fn rejects_hyphen() {
1417        assert!(!is_valid_capability_slug("bank-officer"));
1418    }
1419
1420    #[test]
1421    fn rejects_empty_segments() {
1422        assert!(!is_valid_capability_slug("bank..a"));
1423        assert!(!is_valid_capability_slug(".admin"));
1424        assert!(!is_valid_capability_slug("admin."));
1425    }
1426
1427    #[test]
1428    fn rejects_special_chars() {
1429        assert!(!is_valid_capability_slug("admin@read"));
1430        assert!(!is_valid_capability_slug("admin/read"));
1431        assert!(!is_valid_capability_slug("admin read"));
1432    }
1433}
1434
1435// ── Parser ───────────────────────────────────────────────────────────────────
1436
1437pub struct Parser {
1438    tokens: Vec<Token>,
1439    pos: usize,
1440    /// §Fase 119.f — declarations lifted out of a FLOW BODY to program level.
1441    ///
1442    /// README nests an epistemic block inside a flow to scope the helper
1443    /// flows it calls:
1444    ///
1445    /// ```text
1446    /// flow MarketIntelligence(sector: String) -> Report {
1447    ///     know { flow GatherData(sector: String) -> DataSet { … } }
1448    ///     par { … }
1449    /// }
1450    /// ```
1451    ///
1452    /// A top-level `know { … }` already HOISTS its children into the
1453    /// program-level IR collections, stamping `epistemic_mode` on each
1454    /// (`ir_generator`, §99.d/§105/§110). Hoisting the nested one to a
1455    /// top-level `Declaration::Epistemic` therefore makes it byte-identical
1456    /// to the form that already works — zero new handling in the checker, the
1457    /// IR generator, or the runtime. The alternative (a new FlowStep variant
1458    /// carrying declarations) would fork every one of those.
1459    hoisted: Vec<Declaration>,
1460    /// Fase 14.a — leading trivia parallel array, indexed by the
1461    /// effective-token position. `leading_trivia[i]` is the comment
1462    /// trivia that appeared between the previous effective token (or
1463    /// file start) and `tokens[i]`.
1464    leading_trivia: Vec<Vec<Trivia>>,
1465    /// Fase 14.a — trailing trivia parallel array. `trailing_trivia[i]`
1466    /// is the comment trivia on the same line as `tokens[i]`, before
1467    /// the next effective token. Populated by the constructor.
1468    trailing_trivia: Vec<Vec<Trivia>>,
1469    /// Fase 17.a — side-channel for tagging let value_kind. Set by
1470    /// `parse_let_atom` / `parse_let_value_expr` as they descend; read
1471    /// at the end of `parse_let` and stored on the LetStatement.
1472    last_let_value_kind: String,
1473    /// Fase 19.e — loop nesting depth for break/continue scope check.
1474    /// Incremented at the start of `parse_for_in`, decremented after.
1475    /// `parse_break`/`parse_continue` raise ParseError when this is
1476    /// zero (the keyword has no meaning outside a loop body).
1477    loop_depth: u32,
1478    /// §Fase 28.d — Optional source text + filename for the rustc-
1479    /// style source-context block on `ParseError`. Set via the
1480    /// fluent `Parser::with_source` builder; default `None` keeps
1481    /// existing callers (`Parser::new(tokens).parse()`) emitting
1482    /// the legacy single-line shape.
1483    source: Option<String>,
1484    filename: String,
1485}
1486
1487impl Parser {
1488    pub fn new(raw_tokens: Vec<Token>) -> Self {
1489        // ── Fase 14.a — split the raw token stream into:
1490        //   - effective tokens the grammar consumes (cursor advances
1491        //     over these as before),
1492        //   - parallel `leading_trivia` / `trailing_trivia` arrays
1493        //     indexed by effective-token position.
1494        // Comments on a fresh line attach as leading trivia of the
1495        // next effective token; comments on the same line as an
1496        // effective token attach as trailing trivia of that token.
1497        // Roslyn/Swift convention.
1498        let mut effective: Vec<Token> = Vec::with_capacity(raw_tokens.len());
1499        let mut leading: Vec<Vec<Trivia>> = Vec::with_capacity(raw_tokens.len());
1500        let mut trailing: Vec<Vec<Trivia>> = Vec::with_capacity(raw_tokens.len());
1501
1502        let mut pending_leading: Vec<Trivia> = Vec::new();
1503        let mut last_effective_line: i64 = -1;
1504        for tok in raw_tokens {
1505            if is_comment_token(&tok.ttype) {
1506                let kind = token_to_trivia_kind(&tok.ttype)
1507                    .expect("comment token must map to a trivia kind");
1508                let triv = Trivia {
1509                    kind,
1510                    text: tok.value,
1511                    line: tok.line,
1512                    column: tok.column,
1513                };
1514                if !effective.is_empty() && (tok.line as i64) == last_effective_line {
1515                    trailing.last_mut().unwrap().push(triv);
1516                } else {
1517                    pending_leading.push(triv);
1518                }
1519            } else {
1520                last_effective_line = tok.line as i64;
1521                effective.push(tok);
1522                leading.push(std::mem::take(&mut pending_leading));
1523                trailing.push(Vec::new());
1524            }
1525        }
1526
1527        Parser {
1528            hoisted: Vec::new(),
1529            tokens: effective,
1530            pos: 0,
1531            leading_trivia: leading,
1532            trailing_trivia: trailing,
1533            last_let_value_kind: "literal".to_string(),
1534            loop_depth: 0,
1535            source: None,
1536            filename: "<source>".to_string(),
1537        }
1538    }
1539
1540    /// §Fase 28.d — Fluent attach of source text + filename for
1541    /// rustc-style source-context blocks on emitted `ParseError`s.
1542    /// Returns `self` so it chains with `.parse_with_recovery()`:
1543    ///
1544    /// ```ignore
1545    /// let result = Parser::new(tokens)
1546    ///     .with_source(src, "foo.axon")
1547    ///     .parse_with_recovery();
1548    /// ```
1549    ///
1550    /// No-op of any other behaviour — pure metadata attach.
1551    #[must_use]
1552    pub fn with_source(mut self, source: &str, filename: &str) -> Self {
1553        self.source = Some(source.to_string());
1554        self.filename = filename.to_string();
1555        self
1556    }
1557
1558    // ── public API ───────────────────────────────────────────────
1559
1560    pub fn parse(&mut self) -> Result<Program, ParseError> {
1561        let mut program = Program {
1562            declarations: Vec::new(),
1563            declaration_trivia: Vec::new(),
1564            loc: Loc { line: 1, column: 1 },
1565        };
1566        while !self.check(TokenType::Eof) {
1567            // Capture trivia around the declaration. `start_pos` is
1568            // the effective-token position of the declaration's first
1569            // token; that position carries the leading trivia. After
1570            // parsing, `pos - 1` is the last token consumed; that
1571            // position carries the trailing trivia.
1572            let start_pos = self.pos;
1573            let mut decl = match self.parse_declaration() {
1574                Ok(d) => d,
1575                Err(e) => return Err(self.attach_source_to_error(e)),
1576            };
1577            let end_pos = self.pos.saturating_sub(1);
1578            let leading = self
1579                .leading_trivia
1580                .get(start_pos)
1581                .cloned()
1582                .unwrap_or_default();
1583            let trailing = self
1584                .trailing_trivia
1585                .get(end_pos)
1586                .cloned()
1587                .unwrap_or_default();
1588            // Fase 14.b — also copy trivia into the per-struct fields on
1589            // the declaration so consumers can read `flow.leading_trivia`
1590            // directly without going through `program.declaration_trivia[i]`.
1591            // The side-channel is preserved for backward compat with
1592            // 14.a callers and as a flat enumeration source.
1593            attach_trivia_to_decl(&mut decl, leading.clone(), trailing.clone());
1594            program.declarations.push(decl);
1595            program
1596                .declaration_trivia
1597                .push(DeclarationTrivia { leading, trailing });
1598            // §Fase 119.f — drain anything a flow body hoisted to program
1599            // level. Appended AFTER the enclosing declaration so source order
1600            // still reads top-to-bottom in `axon desugar`.
1601            for hoisted in std::mem::take(&mut self.hoisted) {
1602                program.declarations.push(hoisted);
1603                program.declaration_trivia.push(DeclarationTrivia {
1604                    leading: Vec::new(),
1605                    trailing: Vec::new(),
1606                });
1607            }
1608        }
1609        // §Fase 80.g — expand `voice` declarations FIRST (they may emit
1610        // `from Preset@vN` upstream legs), then §80.f preset references,
1611        // BEFORE type-check — so the §80.c laws and the IR see the expanded
1612        // program (and `axon desugar` prints exactly this lowering).
1613        // Unknown presets stay unexpanded — the checker reports them with
1614        // the catalog list (accumulating diagnostics beat a parse abort).
1615        crate::voice_desugar::expand(&mut program);
1616        crate::upstream_presets::expand(&mut program);
1617        Ok(program)
1618    }
1619
1620    // ── §Fase 28.c — recovery-mode parse ─────────────────────────
1621    //
1622    // Mirror of Python's `Parser.parse_with_recovery` from
1623    // `axon/compiler/parser.py`. Wraps `parse_declaration` in a
1624    // try/recover loop: on any `ParseError` the error is appended to
1625    // the list and the cursor advances to the next sync point, then
1626    // parsing resumes. The two stacks must produce structurally
1627    // identical error lists on the same input — that is the cross-
1628    // stack drift gate (D7). See the test module
1629    // `tests::fase28_recovery_tests` and Python-side
1630    // `tests/test_fase28_parser_recovery.py`.
1631
1632    /// Recovery-mode parse. Collects every parse error in source
1633    /// order; the existing `parse()` API remains fail-fast (D9).
1634    ///
1635    /// # Recovery contract (D2)
1636    ///
1637    /// On `ParseError`:
1638    ///   1. Push the error onto `errors`.
1639    ///   2. If the cursor is already on a top-level declaration
1640    ///      keyword (and brace-depth ≤ 0), do not consume — the
1641    ///      caller should retry the declaration parse from here.
1642    ///      Otherwise advance one token to make progress, then
1643    ///      walk to the next sync point.
1644    ///   3. Resume the outer loop.
1645    ///
1646    /// Sync points: top-level declaration keyword at brace-depth ≤ 0,
1647    /// or EOF. Negative depths are treated identically to ≤ 0 — the
1648    /// walker keeps walking through over-balanced `}` rather than
1649    /// pretending a closing brace is itself a sync point (which would
1650    /// emit a ghost "Unexpected token at top level" error in the
1651    /// outer loop).
1652    pub fn parse_with_recovery(&mut self) -> ParseResult {
1653        let mut program = Program {
1654            declarations: Vec::new(),
1655            declaration_trivia: Vec::new(),
1656            loc: Loc { line: 1, column: 1 },
1657        };
1658        let mut errors: Vec<ParseError> = Vec::new();
1659
1660        while !self.check(TokenType::Eof) {
1661            let start_pos = self.pos;
1662            match self.parse_declaration() {
1663                Ok(mut decl) => {
1664                    let end_pos = self.pos.saturating_sub(1);
1665                    let leading = self
1666                        .leading_trivia
1667                        .get(start_pos)
1668                        .cloned()
1669                        .unwrap_or_default();
1670                    let trailing = self
1671                        .trailing_trivia
1672                        .get(end_pos)
1673                        .cloned()
1674                        .unwrap_or_default();
1675                    attach_trivia_to_decl(&mut decl, leading.clone(), trailing.clone());
1676                    program.declarations.push(decl);
1677                    program
1678                        .declaration_trivia
1679                        .push(DeclarationTrivia { leading, trailing });
1680                }
1681                Err(err) => {
1682                    // §Fase 28.d — attach source-context block when a
1683                    // source has been provided via `with_source(...)`;
1684                    // otherwise the error keeps its single-line shape.
1685                    errors.push(self.attach_source_to_error(err));
1686                    // Make progress. If parse_declaration returned
1687                    // immediately on the same token (e.g. unknown
1688                    // top-level token), we MUST advance at least one
1689                    // token to avoid an infinite loop.
1690                    if self.pos == start_pos && !self.check(TokenType::Eof) {
1691                        self.advance();
1692                    }
1693                    self.advance_to_sync_point();
1694                }
1695            }
1696        }
1697
1698        ParseResult { program, errors }
1699    }
1700
1701    /// §Fase 28.d — Decorate a `ParseError` with a `SourceSnippet`
1702    /// when the parser has source context attached, otherwise return
1703    /// the error unchanged. Idempotent: if the error already carries
1704    /// a snippet, this overwrites it with the parser's source.
1705    fn attach_source_to_error(&self, err: ParseError) -> ParseError {
1706        match &self.source {
1707            Some(src) if err.line >= 1 => err.attach_source(src, &self.filename),
1708            _ => err,
1709        }
1710    }
1711
1712    /// §Fase 28.c — Walk the cursor forward until the next sync
1713    /// point (top-level declaration keyword at brace-depth ≤ 0) or
1714    /// EOF. Used by `parse_with_recovery` to skip the malformed
1715    /// remainder of a failed declaration.
1716    fn advance_to_sync_point(&mut self) {
1717        let mut depth: i32 = 0;
1718        while !self.check(TokenType::Eof) {
1719            let tt = self.current().ttype.clone();
1720            // Sync at top-level keywords when depth ≤ 0. We do not
1721            // consume the keyword — the outer loop will dispatch on
1722            // it.
1723            if is_top_level_decl_kw_for_recovery(&tt) && depth <= 0 {
1724                return;
1725            }
1726            if matches!(tt, TokenType::LBrace) {
1727                depth += 1;
1728            } else if matches!(tt, TokenType::RBrace) {
1729                depth -= 1;
1730            }
1731            self.advance();
1732        }
1733    }
1734
1735    // ── token helpers ────────────────────────────────────────────
1736
1737    fn current(&self) -> &Token {
1738        if self.pos >= self.tokens.len() {
1739            self.tokens.last().unwrap() // EOF sentinel
1740        } else {
1741            &self.tokens[self.pos]
1742        }
1743    }
1744
1745    fn advance(&mut self) -> &Token {
1746        let idx = self.pos;
1747        if self.pos < self.tokens.len() {
1748            self.pos += 1;
1749        }
1750        &self.tokens[idx]
1751    }
1752
1753    fn check(&self, tt: TokenType) -> bool {
1754        self.current().ttype == tt
1755    }
1756
1757    fn consume(&mut self, expected: TokenType) -> Result<Token, ParseError> {
1758        let tok = self.current().clone();
1759        if tok.ttype != expected {
1760            return Err(ParseError {
1761                message: format!(
1762                    "Expected {:?}, found {:?}('{}')",
1763                    expected, tok.ttype, tok.value
1764                ),
1765                line: tok.line,
1766                column: tok.column,
1767                            ..Default::default()
1768            });
1769        }
1770        self.pos += 1;
1771        Ok(tok)
1772    }
1773
1774    /// §Fase 41.b — build a `ParseError` at the current token's location.
1775    fn error(&self, message: &str) -> ParseError {
1776        let tok = self.current();
1777        ParseError { message: message.to_string(), line: tok.line, column: tok.column, ..Default::default() }
1778    }
1779
1780    /// Consume any identifier or keyword-used-as-value.
1781    fn consume_any_ident_or_kw(&mut self) -> Result<Token, ParseError> {
1782        let tok = self.current().clone();
1783        match tok.ttype {
1784            TokenType::Identifier
1785            | TokenType::Bool
1786            | TokenType::StringLit
1787            | TokenType::Integer
1788            | TokenType::Float => {
1789                self.pos += 1;
1790                Ok(tok)
1791            }
1792            _ => {
1793                // Allow any keyword token whose value is alphabetic
1794                if !tok.value.is_empty()
1795                    && tok.value.chars().all(|c| c.is_alphanumeric() || c == '_')
1796                    && tok.ttype != TokenType::Eof
1797                {
1798                    self.pos += 1;
1799                    Ok(tok)
1800                } else {
1801                    Err(ParseError {
1802                        message: format!(
1803                            "Expected identifier or keyword value, found {:?}('{}')",
1804                            tok.ttype, tok.value
1805                        ),
1806                        line: tok.line,
1807                        column: tok.column,
1808                                            ..Default::default()
1809                    })
1810                }
1811            }
1812        }
1813    }
1814
1815    fn consume_number(&mut self) -> Result<f64, ParseError> {
1816        let tok = self.current().clone();
1817        match tok.ttype {
1818            TokenType::Float | TokenType::Integer => {
1819                self.pos += 1;
1820                tok.value.parse::<f64>().map_err(|_| ParseError {
1821                    message: format!("Invalid number '{}'", tok.value),
1822                    line: tok.line,
1823                    column: tok.column,
1824                                    ..Default::default()
1825                })
1826            }
1827            _ => Err(ParseError {
1828                message: format!("Expected number, found {:?}('{}')", tok.ttype, tok.value),
1829                line: tok.line,
1830                column: tok.column,
1831                            ..Default::default()
1832            }),
1833        }
1834    }
1835
1836    fn parse_bool(&mut self) -> Result<bool, ParseError> {
1837        let tok = self.consume(TokenType::Bool)?;
1838        Ok(tok.value == "true")
1839    }
1840
1841    fn loc_of(&self, tok: &Token) -> Loc {
1842        Loc {
1843            line: tok.line,
1844            column: tok.column,
1845        }
1846    }
1847
1848    fn check_run_modifier(&self) -> bool {
1849        matches!(
1850            self.current().ttype,
1851            TokenType::As
1852                | TokenType::Within
1853                | TokenType::ConstrainedBy
1854                | TokenType::OnFailure
1855                | TokenType::OutputTo
1856                | TokenType::Effort
1857        )
1858    }
1859
1860    // ── list helpers ─────────────────────────────────────────────
1861
1862    fn parse_string_list(&mut self) -> Result<Vec<String>, ParseError> {
1863        self.consume(TokenType::LBracket)?;
1864        let mut items = Vec::new();
1865        items.push(self.consume(TokenType::StringLit)?.value);
1866        while self.check(TokenType::Comma) {
1867            self.advance();
1868            items.push(self.consume(TokenType::StringLit)?.value);
1869        }
1870        self.consume(TokenType::RBracket)?;
1871        Ok(items)
1872    }
1873
1874    /// §Fase 83.a — a bracketed list of quoted string literals, tolerant of
1875    /// an empty `[]` and a trailing comma before `]` (the `Window.exclude`
1876    /// shape, generalized into a reusable helper). Used for CORS field
1877    /// lists whose values contain characters (`://`, `.`, `-`) that aren't
1878    /// valid bare identifiers — `allow_origins`, `allow_headers`,
1879    /// `expose_headers` — where `parse_string_list`'s "at least one item,
1880    /// no trailing comma" strictness would reject a legitimate empty or
1881    /// comma-terminated declaration.
1882    fn parse_bracketed_strings(&mut self) -> Result<Vec<String>, ParseError> {
1883        self.consume(TokenType::LBracket)?;
1884        let mut items = Vec::new();
1885        if !self.check(TokenType::RBracket) {
1886            items.push(self.consume(TokenType::StringLit)?.value);
1887            while self.check(TokenType::Comma) {
1888                self.advance();
1889                if self.check(TokenType::RBracket) {
1890                    break; // trailing comma
1891                }
1892                items.push(self.consume(TokenType::StringLit)?.value);
1893            }
1894        }
1895        self.consume(TokenType::RBracket)?;
1896        Ok(items)
1897    }
1898
1899    fn parse_identifier_list(&mut self) -> Result<Vec<String>, ParseError> {
1900        let mut names = Vec::new();
1901        names.push(self.consume(TokenType::Identifier)?.value);
1902        while self.check(TokenType::Comma) {
1903            self.advance();
1904            names.push(self.consume(TokenType::Identifier)?.value);
1905        }
1906        Ok(names)
1907    }
1908
1909    fn parse_bracketed_identifiers(&mut self) -> Result<Vec<String>, ParseError> {
1910        self.consume(TokenType::LBracket)?;
1911        let items = self.parse_extended_identifier_list()?;
1912        self.consume(TokenType::RBracket)?;
1913        Ok(items)
1914    }
1915
1916    fn parse_extended_identifier_list(&mut self) -> Result<Vec<String>, ParseError> {
1917        let mut items = Vec::new();
1918        items.push(self.consume_any_ident_or_kw()?.value);
1919        while self.check(TokenType::Comma) {
1920            self.advance();
1921            items.push(self.consume_any_ident_or_kw()?.value);
1922        }
1923        Ok(items)
1924    }
1925
1926    fn parse_dotted_identifier(&mut self) -> Result<String, ParseError> {
1927        let mut parts = vec![self.consume_any_ident_or_kw()?.value];
1928        while self.check(TokenType::Dot) {
1929            self.advance();
1930            parts.push(self.consume_any_ident_or_kw()?.value);
1931        }
1932        Ok(parts.join("."))
1933    }
1934
1935    fn parse_expression_string(&mut self) -> Result<String, ParseError> {
1936        if self.check(TokenType::LBracket) {
1937            let items = self.parse_bracketed_dot_identifiers()?;
1938            return Ok(format!("[{}]", items.join(", ")));
1939        }
1940        self.parse_dotted_identifier()
1941    }
1942
1943    fn parse_bracketed_dot_identifiers(&mut self) -> Result<Vec<String>, ParseError> {
1944        self.consume(TokenType::LBracket)?;
1945        let mut items = vec![self.parse_dotted_identifier()?];
1946        while self.check(TokenType::Comma) {
1947            self.advance();
1948            items.push(self.parse_dotted_identifier()?);
1949        }
1950        self.consume(TokenType::RBracket)?;
1951        Ok(items)
1952    }
1953
1954    fn parse_argument_list(&mut self) -> Result<Vec<String>, ParseError> {
1955        let mut args = Vec::new();
1956        while !self.check(TokenType::RParen) {
1957            let tok = self.current().clone();
1958            match tok.ttype {
1959                TokenType::StringLit | TokenType::Integer | TokenType::Float => {
1960                    self.advance();
1961                    args.push(tok.value);
1962                }
1963                TokenType::Identifier => {
1964                    self.advance();
1965                    let mut val = tok.value;
1966                    if self.check(TokenType::Dot) {
1967                        self.advance();
1968                        val.push('.');
1969                        val.push_str(&self.consume_any_ident_or_kw()?.value);
1970                    }
1971                    args.push(val);
1972                }
1973                _ => {
1974                    self.advance();
1975                    let key = tok.value;
1976                    if self.check(TokenType::Colon) {
1977                        self.advance();
1978                        let v = self.advance().value.clone();
1979                        args.push(format!("{key}:{v}"));
1980                    } else {
1981                        args.push(key);
1982                    }
1983                }
1984            }
1985            if self.check(TokenType::Comma) {
1986                self.advance();
1987            }
1988        }
1989        Ok(args)
1990    }
1991
1992    /// Skip a single value or balanced bracketed/braced block (unknown field).
1993    fn skip_value(&mut self) {
1994        match self.current().ttype {
1995            TokenType::LBracket => {
1996                self.advance();
1997                let mut depth = 1u32;
1998                while depth > 0 && !self.check(TokenType::Eof) {
1999                    if self.check(TokenType::LBracket) {
2000                        depth += 1;
2001                    } else if self.check(TokenType::RBracket) {
2002                        depth -= 1;
2003                    }
2004                    self.advance();
2005                }
2006            }
2007            TokenType::LBrace => {
2008                self.advance();
2009                let mut depth = 1u32;
2010                while depth > 0 && !self.check(TokenType::Eof) {
2011                    if self.check(TokenType::LBrace) {
2012                        depth += 1;
2013                    } else if self.check(TokenType::RBrace) {
2014                        depth -= 1;
2015                    }
2016                    self.advance();
2017                }
2018            }
2019            TokenType::Lt => {
2020                // effect row: <io, network, ...>
2021                self.advance();
2022                let mut depth = 1u32;
2023                while depth > 0 && !self.check(TokenType::Eof) {
2024                    if self.check(TokenType::Lt) {
2025                        depth += 1;
2026                    } else if self.check(TokenType::Gt) {
2027                        depth -= 1;
2028                    }
2029                    self.advance();
2030                }
2031            }
2032            _ => {
2033                self.advance();
2034                while self.check(TokenType::Dot) {
2035                    self.advance();
2036                    self.advance();
2037                }
2038            }
2039        }
2040    }
2041
2042    /// Skip a balanced `{ ... }` block including its braces.
2043    fn skip_braced_block(&mut self) -> Result<(), ParseError> {
2044        self.consume(TokenType::LBrace)?;
2045        let mut depth = 1u32;
2046        while depth > 0 {
2047            if self.check(TokenType::Eof) {
2048                let tok = self.current();
2049                return Err(ParseError {
2050                    message: "Unterminated block — expected '}'".to_string(),
2051                    line: tok.line,
2052                    column: tok.column,
2053                                    ..Default::default()
2054                });
2055            }
2056            if self.check(TokenType::LBrace) {
2057                depth += 1;
2058            } else if self.check(TokenType::RBrace) {
2059                depth -= 1;
2060            }
2061            self.advance();
2062        }
2063        Ok(())
2064    }
2065
2066    fn at_declaration_start(&self) -> bool {
2067        is_declaration_keyword(&self.current().ttype) || self.check(TokenType::Eof)
2068    }
2069
2070    // ── top-level dispatch ───────────────────────────────────────
2071
2072    fn parse_declaration(&mut self) -> Result<Declaration, ParseError> {
2073        let tok = self.current().clone();
2074
2075        // §Fase 114.a — a TOP-LEVEL `budget <Name> { … }`.
2076        //
2077        // `budget` lexes as `TokenType::Budget` (the daemon-field keyword). At top
2078        // level it is only a declaration when a NAME follows — `budget Foo { … }`.
2079        // The lookahead is what keeps the daemon-attached form (`daemon D { budget
2080        // { … } }`, where `{` follows immediately) untouched: there the next token
2081        // is `{`, not an identifier, so this branch does not fire.
2082        if tok.ttype == TokenType::Budget && self.peek_is_identifier() {
2083            return self.parse_top_level_budget().map(Declaration::Budget);
2084        }
2085
2086        match tok.ttype {
2087            TokenType::Import => self.parse_import().map(Declaration::Import),
2088            TokenType::Persona => self.parse_persona().map(Declaration::Persona),
2089            TokenType::Context => self.parse_context().map(Declaration::Context),
2090            TokenType::Anchor => self.parse_anchor().map(Declaration::Anchor),
2091            TokenType::Memory => self.parse_memory().map(Declaration::Memory),
2092            TokenType::Tool => self.parse_tool().map(Declaration::Tool),
2093            TokenType::Type => self.parse_type_def().map(Declaration::Type),
2094            TokenType::Flow => self.parse_flow().map(Declaration::Flow),
2095            TokenType::Intent => self.parse_intent().map(Declaration::Intent),
2096            TokenType::Run => self.parse_run().map(Declaration::Run),
2097            TokenType::Let => self.parse_let().map(Declaration::Let),
2098            TokenType::Know | TokenType::Believe | TokenType::Speculate | TokenType::Doubt => {
2099                self.parse_epistemic_block().map(Declaration::Epistemic)
2100            }
2101            TokenType::Lambda => self.parse_lambda_data().map(Declaration::LambdaData),
2102
2103            // ── Tier 2 declarations (full AST) ──────────────────
2104            TokenType::Agent => self.parse_agent().map(Declaration::Agent),
2105            TokenType::Shield => self.parse_shield().map(Declaration::Shield),
2106            // §Fase 71.a — temporal execution-window guard.
2107            TokenType::Window => self.parse_window().map(Declaration::Window),
2108            TokenType::Pix => self.parse_pix().map(Declaration::Pix),
2109            TokenType::Ledger => self.parse_ledger().map(Declaration::Ledger),
2110            TokenType::Psyche => self.parse_psyche().map(Declaration::Psyche),
2111            TokenType::Corpus => self.parse_corpus().map(Declaration::Corpus),
2112            TokenType::Dataspace => self.parse_dataspace().map(Declaration::Dataspace),
2113            TokenType::Ots => self.parse_ots().map(Declaration::Ots),
2114            TokenType::Mandate => self.parse_mandate().map(Declaration::Mandate),
2115            TokenType::Compute => self.parse_compute().map(Declaration::Compute),
2116            TokenType::Daemon => self.parse_daemon().map(Declaration::Daemon),
2117            TokenType::Extension => self.parse_extension().map(Declaration::Extension),
2118            TokenType::AxonStore => self.parse_axonstore().map(Declaration::AxonStore),
2119            TokenType::AxonEndpoint => self.parse_axonendpoint().map(Declaration::AxonEndpoint),
2120
2121            // ── §λ-L-E Fase 1 — I/O cognitivo ───────────────────
2122            TokenType::Resource => self.parse_resource().map(Declaration::Resource),
2123            TokenType::Fabric => self.parse_fabric().map(Declaration::Fabric),
2124            TokenType::Manifest => self.parse_manifest().map(Declaration::Manifest),
2125            TokenType::Observe => self.parse_observe().map(Declaration::Observe),
2126
2127            // ── §λ-L-E Fase 3 — Control cognitivo ───────────────
2128            TokenType::Reconcile => self.parse_reconcile().map(Declaration::Reconcile),
2129            TokenType::Lease => self.parse_lease().map(Declaration::Lease),
2130            TokenType::Ensemble => self.parse_ensemble().map(Declaration::Ensemble),
2131
2132            // ── §λ-L-E Fase 4 — Topology + π-calculus sessions ─
2133            TokenType::Session => self.parse_session_definition().map(Declaration::Session),
2134            TokenType::Topology => self.parse_topology().map(Declaration::Topology),
2135
2136            // ── §Fase 41.b — typed WebSocket transport ─────────
2137            TokenType::Socket => self.parse_socket().map(Declaration::Socket),
2138
2139            // ── §Fase 80.b — outbound vendor connection ─────────
2140            TokenType::Upstream => self.parse_upstream().map(Declaration::Upstream),
2141
2142            // ── §Fase 80.g — the voice-agent simplicity layer ───
2143            TokenType::Voice => self.parse_voice().map(Declaration::Voice),
2144
2145            // ── §Fase 83.a — the named origin-policy declaration ─
2146            TokenType::Cors => self.parse_cors().map(Declaration::Cors),
2147
2148            // ── §Fase 85.a — the named result-memoization policy ─
2149            TokenType::Cache => self.parse_cache().map(Declaration::Cache),
2150            TokenType::Document => self.parse_document().map(Declaration::Document),
2151
2152            // ── §Fase 105 — Governed CRM Delivery ─
2153            TokenType::Deliver => self.parse_deliver().map(Declaration::Deliver),
2154            TokenType::Notify => self.parse_notify().map(Declaration::Notify),
2155
2156            // ── §Fase 87.a — the long-horizon autonomous research primitive ─
2157            TokenType::Savant => self.parse_savant().map(Declaration::Savant),
2158
2159            // ── §Fase 87.d — the dynamic tool-synthesis policy ──────────────
2160            TokenType::Synth => self.parse_synth().map(Declaration::Synth),
2161
2162            // ── §Fase 88.a — the authorization-scope policy declaration ─────
2163            TokenType::Scope => self.parse_scope().map(Declaration::Scope),
2164
2165            // ── §Fase 92.a — the ephemeral-credential contract ──────────────
2166            TokenType::Credential => self.parse_credential().map(Declaration::Credential),
2167
2168            // ── §Fase 51.c.2 — Pauli-sum observable ────────────
2169            TokenType::Observable => self.parse_observable().map(Declaration::Observable),
2170
2171            // ── §Fase 69.a — Advantage Witness ──────────────────
2172            TokenType::Witness => self.parse_witness().map(Declaration::Witness),
2173
2174            // ── §λ-L-E Fase 5 — Cognitive immune system ─────────
2175            TokenType::Immune => self.parse_immune().map(Declaration::Immune),
2176            TokenType::Reflex => self.parse_reflex().map(Declaration::Reflex),
2177            TokenType::Heal => self.parse_heal().map(Declaration::Heal),
2178
2179            // ── §λ-L-E Fase 9 — UI cognitiva ────────────────────
2180            TokenType::Component => self.parse_component().map(Declaration::Component),
2181            TokenType::View => self.parse_view().map(Declaration::View),
2182
2183            // ── §λ-L-E Fase 13 — Mobile typed channels ──────────
2184            TokenType::Channel => self.parse_channel().map(Declaration::Channel),
2185
2186            // ── Tier 3+ structural fallback ─────────────────────
2187            // Store operations: keyword target { ... } or keyword target ...
2188            TokenType::Ingest
2189            | TokenType::Persist
2190            | TokenType::Retrieve
2191            | TokenType::Mutate
2192            | TokenType::Purge
2193            | TokenType::Transact => self.parse_generic_declaration(),
2194
2195            // MCP declaration
2196            TokenType::Mcp => self.parse_generic_declaration(),
2197
2198            _ => {
2199                // §Fase 28.e — append "Did you mean X?" hint when the
2200                // unknown token looks like a typo'd top-level keyword
2201                // (Levenshtein ≤ 2). D3, D11 ratified 2026-05-10.
2202                let hint = crate::smart_suggest::suggest_for(
2203                    &tok.value,
2204                    crate::smart_suggest::TOP_LEVEL_KEYWORD_NAMES,
2205                );
2206                let base = format!(
2207                    "Unexpected token at top level: '{}' — expected declaration \
2208                     (persona, context, anchor, flow, run, ...)",
2209                    tok.value
2210                );
2211                let message = if hint.is_empty() {
2212                    base
2213                } else {
2214                    format!("{base}. {hint}")
2215                };
2216                Err(ParseError {
2217                    message,
2218                    line: tok.line,
2219                    column: tok.column,
2220                    ..Default::default()
2221                })
2222            }
2223        }
2224    }
2225
2226    // ── IMPORT ───────────────────────────────────────────────────
2227
2228    fn parse_import(&mut self) -> Result<ImportNode, ParseError> {
2229        let tok = self.consume(TokenType::Import)?;
2230        let loc = self.loc_of(&tok);
2231
2232        let mut path_parts = Vec::new();
2233
2234        // Optional @ scope
2235        if self.check(TokenType::At) {
2236            self.advance();
2237            let first = self.consume(TokenType::Identifier)?;
2238            path_parts.push(format!("@{}", first.value));
2239        } else {
2240            let first = self.consume(TokenType::Identifier)?;
2241            path_parts.push(first.value);
2242        }
2243
2244        while self.check(TokenType::Dot) {
2245            self.advance();
2246            if self.check(TokenType::LBrace) {
2247                break;
2248            }
2249            let part = self.consume(TokenType::Identifier)?;
2250            path_parts.push(part.value);
2251        }
2252
2253        let mut names = Vec::new();
2254        if self.check(TokenType::LBrace) {
2255            self.advance();
2256            names = self.parse_identifier_list()?;
2257            self.consume(TokenType::RBrace)?;
2258        }
2259
2260        // ── §Fase 115.c — the `@allow_downgrade` ECC valve ───────────────
2261        //
2262        // `import a.b.{X} @allow_downgrade` acknowledges an epistemic
2263        // downgrade across this edge (see `epistemic_compat.rs`). The
2264        // annotation position is unambiguous: no top-level declaration
2265        // begins with `@`, so an `@` here belongs to this import — and an
2266        // unknown annotation is refused with the fix in the message
2267        // rather than surfacing later as an opaque parse error.
2268        let mut allow_downgrade = false;
2269        if self.check(TokenType::At) {
2270            let at_tok = self.current().clone();
2271            self.advance();
2272            let ident = self.consume(TokenType::Identifier)?;
2273            if ident.value == "allow_downgrade" {
2274                allow_downgrade = true;
2275            } else {
2276                return Err(ParseError {
2277                    message: format!(
2278                        "unknown import annotation '@{}' — the only import annotation is \
2279                         `@allow_downgrade` (the §115 epistemic-downgrade acknowledgment).",
2280                        ident.value
2281                    ),
2282                    line: at_tok.line,
2283                    column: at_tok.column,
2284                    ..Default::default()
2285                });
2286            }
2287        }
2288
2289        // ── §Fase 111 — `apx` is RETRACTED ───────────────────────────────
2290        //
2291        // `import X with apx { … }` used to parse and then call
2292        // `skip_braced_block()` — the policy was consumed and thrown on the
2293        // floor. It never reached the AST, let alone the IR. In `axon-rs` the
2294        // string "apx" occurred only inside comments: there is no APX crate,
2295        // no binary, no MEC/PCC dependency verification, no EPR ranking, no
2296        // quarantine and no compliance gate. The public README advertised all
2297        // five.
2298        //
2299        // A dependency policy that silently evaporates is the worst possible
2300        // shape for this particular promise: the adopter believes their supply
2301        // chain is being verified, which is exactly the belief that stops them
2302        // from verifying it themselves. Refuse, loudly.
2303        let next_is_apx = self
2304            .tokens
2305            .get(self.pos + 1)
2306            .map(|t| t.value == "apx")
2307            .unwrap_or(false);
2308        if self.current().value == "with" && next_is_apx {
2309            let tok = self.current().clone();
2310            return Err(ParseError {
2311                message: "`import … with apx { … }` is RETRACTED (§111). The apx policy block was \
2312                          parsed and silently DISCARDED — it never reached the IR, and no epistemic \
2313                          dependency manager exists: no MEC/PCC verification, no EPR ranking, no \
2314                          quarantine, no compliance gate. Declaring it verified nothing while \
2315                          implying your supply chain was checked. Remove the `with apx { … }` \
2316                          clause; the plain `import` resolves through the §115 Epistemic Module \
2317                          System."
2318                    .to_string(),
2319                line: tok.line,
2320                column: tok.column,
2321                ..Default::default()
2322            });
2323        }
2324
2325        Ok(ImportNode {
2326            module_path: path_parts,
2327            names,
2328            allow_downgrade,
2329            loc,
2330            leading_trivia: Vec::new(),
2331            trailing_trivia: Vec::new(),
2332        })
2333    }
2334
2335    // ── PERSONA ──────────────────────────────────────────────────
2336
2337    fn parse_persona(&mut self) -> Result<PersonaDefinition, ParseError> {
2338        let tok = self.consume(TokenType::Persona)?;
2339        let loc = self.loc_of(&tok);
2340        let name = self.consume(TokenType::Identifier)?.value;
2341        self.consume(TokenType::LBrace)?;
2342
2343        let mut node = PersonaDefinition {
2344            name,
2345            domain: Vec::new(),
2346            tone: String::new(),
2347            confidence_threshold: None,
2348            cite_sources: None,
2349            refuse_if: Vec::new(),
2350            language: String::new(),
2351            description: String::new(),
2352            loc,
2353            leading_trivia: Vec::new(),
2354            trailing_trivia: Vec::new(),
2355        };
2356
2357        while !self.check(TokenType::RBrace) {
2358            let field_name = self.current().value.clone();
2359            self.advance();
2360            self.consume(TokenType::Colon)?;
2361
2362            match field_name.as_str() {
2363                "domain" => node.domain = self.parse_string_list()?,
2364                "tone" => node.tone = self.consume_any_ident_or_kw()?.value,
2365                "confidence_threshold" => node.confidence_threshold = Some(self.consume_number()?),
2366                "cite_sources" => node.cite_sources = Some(self.parse_bool()?),
2367                "refuse_if" => node.refuse_if = self.parse_bracketed_identifiers()?,
2368                "language" => node.language = self.consume(TokenType::StringLit)?.value,
2369                "description" => node.description = self.consume(TokenType::StringLit)?.value,
2370                _ => self.skip_value(),
2371            }
2372        }
2373        self.consume(TokenType::RBrace)?;
2374        Ok(node)
2375    }
2376
2377    // ── CONTEXT ──────────────────────────────────────────────────
2378
2379    fn parse_context(&mut self) -> Result<ContextDefinition, ParseError> {
2380        let tok = self.consume(TokenType::Context)?;
2381        let loc = self.loc_of(&tok);
2382        let name = self.consume(TokenType::Identifier)?.value;
2383        self.consume(TokenType::LBrace)?;
2384
2385        let mut node = ContextDefinition {
2386            name,
2387            memory_scope: String::new(),
2388            language: String::new(),
2389            depth: String::new(),
2390            max_tokens: None,
2391            temperature: None,
2392            cite_sources: None,
2393            now_tz: None,
2394            loc,
2395            leading_trivia: Vec::new(),
2396            trailing_trivia: Vec::new(),
2397        };
2398
2399        while !self.check(TokenType::RBrace) {
2400            let field_name = self.current().value.clone();
2401            self.advance();
2402            self.consume(TokenType::Colon)?;
2403
2404            match field_name.as_str() {
2405                "memory" => node.memory_scope = self.consume_any_ident_or_kw()?.value,
2406                "language" => node.language = self.consume(TokenType::StringLit)?.value,
2407                "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
2408                // §Fase 91.a — the frame's cognitive timezone (IANA string).
2409                "now" => node.now_tz = Some(self.consume(TokenType::StringLit)?.value),
2410                "max_tokens" => {
2411                    node.max_tokens = Some(
2412                        self.consume(TokenType::Integer)?
2413                            .value
2414                            .parse::<i64>()
2415                            .unwrap_or(0),
2416                    )
2417                }
2418                "temperature" => node.temperature = Some(self.consume_number()?),
2419                "cite_sources" => node.cite_sources = Some(self.parse_bool()?),
2420                _ => self.skip_value(),
2421            }
2422        }
2423        self.consume(TokenType::RBrace)?;
2424        Ok(node)
2425    }
2426
2427    // ── ANCHOR ───────────────────────────────────────────────────
2428
2429    fn parse_anchor(&mut self) -> Result<AnchorConstraint, ParseError> {
2430        let tok = self.consume(TokenType::Anchor)?;
2431        let loc = self.loc_of(&tok);
2432        let name = self.consume(TokenType::Identifier)?.value;
2433        self.consume(TokenType::LBrace)?;
2434
2435        let mut node = AnchorConstraint {
2436            name,
2437            require: String::new(),
2438            reject: Vec::new(),
2439            enforce: String::new(),
2440            description: String::new(),
2441            confidence_floor: None,
2442            unknown_response: String::new(),
2443            on_violation: String::new(),
2444            on_violation_target: String::new(),
2445            loc,
2446            leading_trivia: Vec::new(),
2447            trailing_trivia: Vec::new(),
2448        };
2449
2450        while !self.check(TokenType::RBrace) {
2451            let field_name = self.current().value.clone();
2452            self.advance();
2453            self.consume(TokenType::Colon)?;
2454
2455            match field_name.as_str() {
2456                "require" => node.require = self.consume_any_ident_or_kw()?.value,
2457                "description" => node.description = self.consume(TokenType::StringLit)?.value,
2458                "reject" => node.reject = self.parse_bracketed_identifiers()?,
2459                "enforce" => node.enforce = self.consume_any_ident_or_kw()?.value,
2460                "confidence_floor" => node.confidence_floor = Some(self.consume_number()?),
2461                "unknown_response" => {
2462                    node.unknown_response = self.consume(TokenType::StringLit)?.value
2463                }
2464                "on_violation" => {
2465                    // Parse: raise ErrorName | fallback(...) | identifier
2466                    let action = self.consume_any_ident_or_kw()?.value;
2467                    node.on_violation = action.clone();
2468                    if action == "raise" || action == "fallback" {
2469                        node.on_violation_target = self.consume_any_ident_or_kw()?.value;
2470                    }
2471                }
2472                _ => self.skip_value(),
2473            }
2474        }
2475        self.consume(TokenType::RBrace)?;
2476        Ok(node)
2477    }
2478
2479    // ── MEMORY ───────────────────────────────────────────────────
2480
2481    fn parse_memory(&mut self) -> Result<MemoryDefinition, ParseError> {
2482        let tok = self.consume(TokenType::Memory)?;
2483        let loc = self.loc_of(&tok);
2484        let name = self.consume(TokenType::Identifier)?.value;
2485        self.consume(TokenType::LBrace)?;
2486
2487        let mut node = MemoryDefinition {
2488            name,
2489            store: String::new(),
2490            backend: String::new(),
2491            retrieval: String::new(),
2492            decay: String::new(),
2493            loc,
2494            leading_trivia: Vec::new(),
2495            trailing_trivia: Vec::new(),
2496        };
2497
2498        while !self.check(TokenType::RBrace) {
2499            let field_name = self.current().value.clone();
2500            self.advance();
2501            self.consume(TokenType::Colon)?;
2502
2503            match field_name.as_str() {
2504                "store" => node.store = self.consume_any_ident_or_kw()?.value,
2505                "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
2506                "retrieval" => node.retrieval = self.consume_any_ident_or_kw()?.value,
2507                "decay" => {
2508                    if self.check(TokenType::Duration) {
2509                        node.decay = self.advance().value.clone();
2510                    } else {
2511                        node.decay = self.consume_any_ident_or_kw()?.value;
2512                    }
2513                }
2514                _ => self.skip_value(),
2515            }
2516        }
2517        self.consume(TokenType::RBrace)?;
2518        Ok(node)
2519    }
2520
2521    // ── TOOL ─────────────────────────────────────────────────────
2522
2523    fn parse_tool(&mut self) -> Result<ToolDefinition, ParseError> {
2524        let tok = self.consume(TokenType::Tool)?;
2525        let loc = self.loc_of(&tok);
2526        let name = self.consume(TokenType::Identifier)?.value;
2527        self.consume(TokenType::LBrace)?;
2528
2529        let mut node = ToolDefinition {
2530            name,
2531            provider: String::new(),
2532            max_results: None,
2533            filter_expr: String::new(),
2534            timeout: String::new(),
2535            runtime: String::new(),
2536            resource_ref: String::new(),
2537            sandbox: None,
2538            effects: None,
2539            parameters: Vec::new(),
2540            output_type: None,
2541            requires: Vec::new(),
2542            secret: String::new(),
2543            secret_partition: String::new(),
2544            target: None,
2545            risk: None,
2546            argv: Vec::new(),
2547            cache: String::new(),
2548            scrape: None,
2549            loc,
2550            leading_trivia: Vec::new(),
2551            trailing_trivia: Vec::new(),
2552        };
2553
2554        // §Fase 84.b/D84.13 — unknown fields are recorded (not silently
2555        // skipped) so a `target:`-bound technician tool can HARD-ERROR on one
2556        // (a typo'd safety field must never quietly disable a guard), while a
2557        // legacy schema-less tool keeps its lenient record-and-skip (zero
2558        // regression). The decision is deferred to after the block is parsed,
2559        // since `target:` may appear after the unknown field.
2560        let mut unknown_fields: Vec<(String, u32, u32)> = Vec::new();
2561
2562        while !self.check(TokenType::RBrace) {
2563            let field_tok = self.current().clone();
2564            let field_name = field_tok.value.clone();
2565            self.advance();
2566            self.consume(TokenType::Colon)?;
2567
2568            match field_name.as_str() {
2569                "provider" => node.provider = self.consume_any_ident_or_kw()?.value,
2570                "max_results" => {
2571                    node.max_results = Some(
2572                        self.consume(TokenType::Integer)?
2573                            .value
2574                            .parse::<i64>()
2575                            .unwrap_or(0),
2576                    )
2577                }
2578                "filter" => node.filter_expr = self.parse_filter_expression()?,
2579                "timeout" => node.timeout = self.consume(TokenType::Duration)?.value,
2580                "runtime" => node.runtime = self.consume_any_ident_or_kw()?.value,
2581                // §Fase 114.c — the `resource` this tool's channel runs on. The
2582                // channel's address, concurrency and lifecycle come from it;
2583                // `runtime:` then names the path within the channel.
2584                "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
2585                "sandbox" => node.sandbox = Some(self.parse_bool()?),
2586                "effects" => node.effects = Some(self.parse_effect_row()?),
2587                // §Fase 58.a — the tool's typed input schema + output type.
2588                "parameters" => node.parameters = self.parse_tool_param_schema()?,
2589                "output_type" => node.output_type = Some(self.parse_output_type_string()?),
2590                // §Fase 116.a (D116.9) — the tool's required authorization
2591                // scopes: bare dot-separated capability slugs, the EXACT
2592                // grammar + charset of `credential.grants` (§92) so the two
2593                // vocabularies are one. `requires: [w_organization_social,
2594                // video.publish]`. Subset coverage is `axon-T956`.
2595                "requires" => {
2596                    let bracket_tok = self.current().clone();
2597                    let items = self.parse_bracketed_dot_identifiers()?;
2598                    for slug in &items {
2599                        if !is_valid_capability_slug(slug) {
2600                            return Err(ParseError {
2601                                message: format!(
2602                                    "Invalid capability slug '{slug}' in tool '{}' \
2603                                     `requires:`. Scope slugs must match \
2604                                     ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — the same \
2605                                     grammar as `credential.grants`. Examples: \
2606                                     `w_organization_social`, `video.publish`.",
2607                                    node.name
2608                                ),
2609                                line: bracket_tok.line,
2610                                column: bracket_tok.column,
2611                                ..Default::default()
2612                            });
2613                        }
2614                    }
2615                    node.requires = items;
2616                }
2617                // §Fase 94.c — the per-tenant secret KEY injected at
2618                // dispatch (`rotation_without_revelation`). Key shape +
2619                // technician exclusion are `axon-T902` (type-checker).
2620                "secret" => node.secret = self.parse_dotted_identifier()?,
2621                // §Fase 95.a — `secret_partition:` names one of this tool's
2622                // own `parameters:` (a bare identifier, NOT dotted — it is a
2623                // parameter reference, not a key). Its runtime value becomes
2624                // a single appended key segment at dispatch. The membership +
2625                // `String`-type + technician laws are `axon-T903`.
2626                "secret_partition" => {
2627                    node.secret_partition = self.consume_any_ident_or_kw()?.value
2628                }
2629                // §Fase 84.b — Remote Hands technician fields.
2630                "target" => node.target = Some(self.consume_any_ident_or_kw()?.value),
2631                "risk" => node.risk = Some(self.consume_any_ident_or_kw()?.value),
2632                // The argv template: a bracketed list of quoted elements
2633                // (`argv: ["ping", "-c", "${count}", "${host}"]`). Reuses the
2634                // CORS list helper (tolerant of `[]` and a trailing comma).
2635                "argv" => node.argv = self.parse_bracketed_strings()?,
2636                // §Fase 85.b — the tool's result-memoization policy reference
2637                // (a declared `cache` name, or the `none` opt-out sentinel).
2638                "cache" => node.cache = self.consume_any_ident_or_kw()?.value,
2639                // §Fase 98.b — the closed-catalog web-acquisition config
2640                // block. `scrape: { engine: …, extract: […], … }`.
2641                "scrape" => node.scrape = Some(self.parse_scrape_spec()?),
2642                _ => {
2643                    unknown_fields.push((field_name, field_tok.line, field_tok.column));
2644                    self.skip_value();
2645                }
2646            }
2647        }
2648        self.consume(TokenType::RBrace)?;
2649
2650        // §Fase 84.b/D84.13 — a `target:`-bound tool opts into strict field
2651        // checking. An unknown field on it is a parse error, mirroring the §83
2652        // `cors`/`voice` closed-catalog discipline — but scoped to the
2653        // technician surface so ordinary tools are untouched.
2654        // §Fase 98.b (D98.2) — a `scrape:`-bearing web-acquisition tool opts
2655        // into the same strictness: a typo'd safety field (e.g. a mis-spelled
2656        // `respect_robots`) must never quietly disable a guard.
2657        if node.target.is_some() || node.scrape.is_some() {
2658            if let Some((field_name, line, column)) = unknown_fields.into_iter().next() {
2659                let (surface, valid) = if node.target.is_some() {
2660                    (
2661                        "technician tool (§Fase 84 D84.13)",
2662                        "provider, parameters, output_type, timeout, effects, target, risk, argv",
2663                    )
2664                } else {
2665                    (
2666                        "web-acquisition tool (§Fase 98 D98.2)",
2667                        "provider, parameters, output_type, timeout, effects, secret, \
2668                         secret_partition, cache, scrape",
2669                    )
2670                };
2671                return Err(ParseError {
2672                    message: format!(
2673                        "unknown field `{field_name}` in {surface} `{}` — this tool uses \
2674                         strict field checking; valid fields: {valid}",
2675                        node.name
2676                    ),
2677                    line,
2678                    column,
2679                    ..Default::default()
2680                });
2681            }
2682        }
2683        Ok(node)
2684    }
2685
2686    /// §Fase 98.b — parse the closed-catalog `scrape: { … }` web-acquisition
2687    /// config sub-block. Every field is optional; an unknown field is a hard
2688    /// parse error (the §83 `cors` closed-catalog discipline). Mirrors the
2689    /// field grammar of `parse_tool` for the scrape-specific keys.
2690    fn parse_scrape_spec(&mut self) -> Result<crate::ast::ScrapeSpec, ParseError> {
2691        let open = self.consume(TokenType::LBrace)?;
2692        let loc = self.loc_of(&open);
2693        let mut spec = crate::ast::ScrapeSpec {
2694            loc,
2695            ..Default::default()
2696        };
2697        while !self.check(TokenType::RBrace) {
2698            let field_tok = self.current().clone();
2699            let field_name = field_tok.value.clone();
2700            self.advance();
2701            self.consume(TokenType::Colon)?;
2702            match field_name.as_str() {
2703                "engine" => spec.engine = Some(self.consume_any_ident_or_kw()?.value),
2704                "impersonate" => spec.impersonate = Some(self.consume_any_ident_or_kw()?.value),
2705                "render_wait" => spec.render_wait = Some(self.consume(TokenType::Duration)?.value),
2706                "proxy" => spec.proxy = self.parse_dotted_identifier()?,
2707                "respect_robots" => spec.respect_robots = Some(self.parse_bool()?),
2708                "extract" => spec.extract = self.parse_bracketed_strings()?,
2709                "adaptive" => spec.adaptive = Some(self.parse_bool()?),
2710                "similarity_floor" => spec.similarity_floor = self.parse_optional_float(),
2711                "follow" => spec.follow = self.consume(TokenType::StringLit)?.value,
2712                "max_depth" => {
2713                    spec.max_depth =
2714                        Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2715                }
2716                "max_pages" => {
2717                    spec.max_pages =
2718                        Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2719                }
2720                "concurrency" => {
2721                    spec.concurrency =
2722                        Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2723                }
2724                "politeness" => spec.politeness = self.consume_any_ident_or_kw()?.value,
2725                "checkpoint" => spec.checkpoint = self.consume_any_ident_or_kw()?.value,
2726                other => {
2727                    return Err(self.error(&format!(
2728                        "unknown scrape field `{other}` — the `scrape: {{ … }}` block is a \
2729                         closed catalog (§Fase 98 D98.2); valid fields: engine, impersonate, \
2730                         render_wait, proxy, respect_robots, extract, adaptive, \
2731                         similarity_floor, follow, max_depth, max_pages, concurrency, \
2732                         politeness, checkpoint"
2733                    )));
2734                }
2735            }
2736        }
2737        self.consume(TokenType::RBrace)?;
2738        Ok(spec)
2739    }
2740
2741    /// §Fase 58.a — parse a tool's INPUT SCHEMA: a brace-delimited list of
2742    /// `name: Type` parameters (`parameters: { query: String, max_results: Int }`).
2743    /// Reuses the flow-parameter shape (`Parameter`), so the same `TypeExpr`
2744    /// grammar — generics like `List<T>`, `?`-optionals — applies. A trailing
2745    /// comma is tolerated; an empty `{}` yields no parameters.
2746    fn parse_tool_param_schema(&mut self) -> Result<Vec<Parameter>, ParseError> {
2747        self.consume(TokenType::LBrace)?;
2748        let mut params = Vec::new();
2749        while !self.check(TokenType::RBrace) {
2750            // Accept a keyword-as-name (`filter`, `type`, `domain`, …) — real
2751            // adopter tool schemas use such parameter names; the `:` after it
2752            // disambiguates.
2753            let name = self.consume_any_ident_or_kw()?;
2754            let ploc = self.loc_of(&name);
2755            self.consume(TokenType::Colon)?;
2756            let type_expr = self.parse_type_expr()?;
2757            params.push(Parameter {
2758                name: name.value,
2759                type_expr,
2760                loc: ploc,
2761            });
2762            if self.check(TokenType::Comma) {
2763                self.advance();
2764            } else {
2765                break;
2766            }
2767        }
2768        self.consume(TokenType::RBrace)?;
2769        Ok(params)
2770    }
2771
2772    fn parse_filter_expression(&mut self) -> Result<String, ParseError> {
2773        let name = self.consume_any_ident_or_kw()?.value;
2774        if self.check(TokenType::LParen) {
2775            self.advance();
2776            let mut parts = vec![name, "(".to_string()];
2777            while !self.check(TokenType::RParen) {
2778                parts.push(self.advance().value.clone());
2779            }
2780            self.consume(TokenType::RParen)?;
2781            parts.push(")".to_string());
2782            Ok(parts.join(""))
2783        } else {
2784            Ok(name)
2785        }
2786    }
2787
2788    fn parse_effect_row(&mut self) -> Result<EffectRow, ParseError> {
2789        let tok = self.consume(TokenType::Lt)?;
2790        let loc = self.loc_of(&tok);
2791        let mut effects = Vec::new();
2792        let mut epistemic_level = String::new();
2793
2794        while !self.check(TokenType::Gt) {
2795            let name = self.consume_any_ident_or_kw()?.value;
2796            if self.check(TokenType::Colon) {
2797                self.advance();
2798                // Fase 11.c / 11.e — qualifiers can be compound slugs
2799                // from a closed catalogue:
2800                //
2801                //   * dot-separated  — `legal:HIPAA.164_502`,
2802                //                       `legal:GDPR.Art6.Consent`,
2803                //                       `legal:PCI_DSS.v4_Req3`
2804                //   * colon-separated — `ots:transform:mulaw8:pcm16`,
2805                //                       `ots:backend:native`
2806                //   * mixed           — supported by the same loop.
2807                //
2808                // The lexer fragments dotted slugs across IDENT /
2809                // INTEGER tokens (e.g., `164_502` lexes as INTEGER
2810                // `164` + IDENT `_502` because `_` starts a fresh
2811                // identifier); we recombine here using source-column
2812                // adjacency so the type checker sees the catalog
2813                // string verbatim.
2814                let level = self.parse_qualifier_value()?;
2815                if name == "epistemic" {
2816                    epistemic_level = level;
2817                } else {
2818                    effects.push(format!("{name}:{level}"));
2819                }
2820            } else {
2821                effects.push(name);
2822            }
2823            if self.check(TokenType::Comma) {
2824                self.advance();
2825            }
2826        }
2827        self.consume(TokenType::Gt)?;
2828
2829        Ok(EffectRow {
2830            effects,
2831            epistemic_level,
2832            loc,
2833        })
2834    }
2835
2836    /// Parse a compound qualifier value following an effect's first
2837    /// colon — supports both dot-separated (`HIPAA.164_502`) and
2838    /// colon-separated (`transform:mulaw8:pcm16`) catalogue slugs, as
2839    /// well as mixed forms.
2840    ///
2841    /// The grammar is: `segment ((`.` | `:`) segment)*` where a
2842    /// segment is a contiguous run of IDENT / INTEGER tokens (see
2843    /// [`Self::consume_dotted_slug_segment`]).
2844    fn parse_qualifier_value(&mut self) -> Result<String, ParseError> {
2845        let mut buf = self.consume_dotted_slug_segment()?;
2846        loop {
2847            let sep = if self.check(TokenType::Dot) {
2848                '.'
2849            } else if self.check(TokenType::Colon) {
2850                ':'
2851            } else {
2852                break;
2853            };
2854            self.advance();
2855            let part = self.consume_dotted_slug_segment()?;
2856            buf.push(sep);
2857            buf.push_str(&part);
2858        }
2859        Ok(buf)
2860    }
2861
2862    /// Consume a contiguous run of IDENT / INTEGER / keyword-ident
2863    /// tokens whose source positions are adjacent (no whitespace
2864    /// between them), concatenating their text into a single segment.
2865    ///
2866    /// Needed for closed-catalogue qualifier slugs whose segment
2867    /// mixes digits and identifier characters — e.g. `HIPAA.164_502`
2868    /// lexes as INTEGER `164` + IDENT `_502` because `_` starts a
2869    /// fresh identifier; the catalog value is the concatenation
2870    /// `164_502`. Adjacency is determined by matching
2871    /// `(line, column + len)` of the previous token against the next
2872    /// token's start position.
2873    fn consume_dotted_slug_segment(&mut self) -> Result<String, ParseError> {
2874        let first = self.consume_any_ident_or_kw()?;
2875        let mut buf = first.value.clone();
2876        let mut next_line = first.line;
2877        let mut next_col = first.column + first.value.chars().count() as u32;
2878        loop {
2879            let cur = self.current();
2880            let is_segment_token = matches!(cur.ttype, TokenType::Identifier | TokenType::Integer,);
2881            if !is_segment_token {
2882                break;
2883            }
2884            if cur.line != next_line || cur.column != next_col {
2885                break;
2886            }
2887            buf.push_str(&cur.value);
2888            next_col = cur.column + cur.value.chars().count() as u32;
2889            next_line = cur.line;
2890            self.pos += 1;
2891        }
2892        Ok(buf)
2893    }
2894
2895    // ── TYPE ─────────────────────────────────────────────────────
2896
2897    fn parse_type_def(&mut self) -> Result<TypeDefinition, ParseError> {
2898        let tok = self.consume(TokenType::Type)?;
2899        let loc = self.loc_of(&tok);
2900        let name = self.consume(TokenType::Identifier)?.value;
2901
2902        let mut node = TypeDefinition {
2903            name,
2904            fields: Vec::new(),
2905            range_constraint: None,
2906            where_clause: None,
2907            compliance: Vec::new(),
2908            loc: loc.clone(),
2909            leading_trivia: Vec::new(),
2910            trailing_trivia: Vec::new(),
2911        };
2912
2913        // Optional range: (0.0..1.0)
2914        if self.check(TokenType::LParen) {
2915            self.advance();
2916            let min_val = self.consume_number()?;
2917            self.consume(TokenType::DotDot)?;
2918            let max_val = self.consume_number()?;
2919            self.consume(TokenType::RParen)?;
2920            node.range_constraint = Some(RangeConstraint {
2921                min_value: min_val,
2922                max_value: max_val,
2923                loc: loc.clone(),
2924            });
2925        }
2926
2927        // Optional where clause
2928        if self.check(TokenType::Where) {
2929            self.advance();
2930            let mut expr_parts = Vec::new();
2931            while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
2932                if self.check(TokenType::Eof) {
2933                    break;
2934                }
2935                expr_parts.push(self.advance().value.clone());
2936            }
2937            node.where_clause = Some(WhereClause {
2938                expression: expr_parts.join(" "),
2939                loc: loc.clone(),
2940            });
2941        }
2942
2943        // Optional ESK Fase 6.1 — `compliance [HIPAA, ...]` prefix modifier
2944        // between `type Name` / `range` / `where` and the body `{`.
2945        if self.check(TokenType::Identifier) && self.current().value == "compliance" {
2946            self.advance();
2947            node.compliance = self.parse_bracketed_identifiers()?;
2948        }
2949
2950        // Optional body: { field: Type, ... }
2951        if self.check(TokenType::LBrace) {
2952            self.advance();
2953            while !self.check(TokenType::RBrace) {
2954                let field_name = self.consume(TokenType::Identifier)?;
2955                let field_loc = self.loc_of(&field_name);
2956                self.consume(TokenType::Colon)?;
2957                let type_expr = self.parse_type_expr()?;
2958                node.fields.push(TypeField {
2959                    name: field_name.value,
2960                    type_expr,
2961                    loc: field_loc,
2962                });
2963                if self.check(TokenType::Comma) {
2964                    self.advance();
2965                }
2966            }
2967            self.consume(TokenType::RBrace)?;
2968        }
2969
2970        Ok(node)
2971    }
2972
2973    fn parse_type_expr(&mut self) -> Result<TypeExpr, ParseError> {
2974        // §Fase 119.c — a LEADING bracket is the list-type sugar the README
2975        // has always written in flow signatures: `readings: [SensorReading]`
2976        // (blocks 44-45). It lowers to exactly what `List<SensorReading>`
2977        // produces, so nothing downstream learns a new shape — the §39.a
2978        // comment below already names `List<T>` as the canonical carrier.
2979        if self.check(TokenType::LBracket) {
2980            let open = self.current().clone();
2981            self.advance();
2982            let inner = self.parse_type_expr()?;
2983            self.consume(TokenType::RBracket)?;
2984            let mut optional = false;
2985            if self.check(TokenType::Question) {
2986                self.advance();
2987                optional = true;
2988            }
2989            return Ok(TypeExpr {
2990                name: "List".to_string(),
2991                generic_param: if inner.generic_param.is_empty() {
2992                    inner.name
2993                } else {
2994                    format!("{}<{}>", inner.name, inner.generic_param)
2995                },
2996                optional,
2997                loc: self.loc_of(&open),
2998            });
2999        }
3000        let name_tok = self.consume(TokenType::Identifier)?;
3001        let loc = self.loc_of(&name_tok);
3002        let mut generic_param = String::new();
3003        let mut optional = false;
3004
3005        if self.check(TokenType::Lt) {
3006            self.advance();
3007            // §Fase 39.a — recursive: the generic param can itself be a
3008            // nested type expression. `FlowEnvelope<List<TenantRecord>>`
3009            // parses as outer=FlowEnvelope, inner=List<TenantRecord>.
3010            // Pre-39.a the inner had to be a single Identifier; nested
3011            // generics like the canonical FlowEnvelope<T> wrapper
3012            // required this lift. Backwards-compat preserved for
3013            // single-level generics like `Stream<Token>` and
3014            // `List<T>` — the recursion lands once and returns the
3015            // same flat string the v1.x parser produced.
3016            let inner = self.parse_type_expr()?;
3017            generic_param = if inner.generic_param.is_empty() {
3018                inner.name
3019            } else {
3020                format!("{}<{}>", inner.name, inner.generic_param)
3021            };
3022            self.consume(TokenType::Gt)?;
3023        }
3024        // §Fase 51.c.3 — bracket type parameters for the continuous-carrier
3025        // grammar: `SymbolicPtr[Tensor[Float32]]`, `DensityMatrix[1024]`. The
3026        // param is either a nested type expression OR a numeric dimension.
3027        if self.check(TokenType::LBracket) {
3028            self.advance();
3029            if matches!(self.current().ttype, TokenType::Integer | TokenType::Float) {
3030                generic_param = self.advance().value.clone();
3031            } else {
3032                let inner = self.parse_type_expr()?;
3033                generic_param = if inner.generic_param.is_empty() {
3034                    inner.name
3035                } else {
3036                    format!("{}[{}]", inner.name, inner.generic_param)
3037                };
3038            }
3039            self.consume(TokenType::RBracket)?;
3040        }
3041        if self.check(TokenType::Question) {
3042            self.advance();
3043            optional = true;
3044        }
3045
3046        Ok(TypeExpr {
3047            name: name_tok.value,
3048            generic_param,
3049            optional,
3050            loc,
3051        })
3052    }
3053
3054    /// Parse a type expression in a context where the AST stores the
3055    /// shape as a flat string (step / reason / forge / ots-apply
3056    /// productions). Mirrors Python `_parse_output_type_string`.
3057    ///
3058    /// Accepts:
3059    /// - `Identifier`        → `"Identifier"`
3060    /// - `Stream<String>`    → `"Stream<String>"`
3061    /// - `Optional?`         → `"Optional?"`
3062    /// - `Stream<String>?`   → `"Stream<String>?"`
3063    ///
3064    /// **Why this exists** — pre-fix, the step parser called
3065    /// `consume(TokenType::Identifier)?.value` which captured only
3066    /// the head identifier and left `< … >` unconsumed. For
3067    /// `output: Stream<Token>`, this produced `output_type =
3068    /// "Stream"`, and downstream `flow_has_stream_output`'s
3069    /// `starts_with("Stream<") && ends_with('>')` predicate then
3070    /// returned false → `implicit_transport == "json"` → the
3071    /// dynamic-route fallback in `axon-rs` served JSON instead of
3072    /// SSE even when the adopter's source canonically declared the
3073    /// algebraic stream effect. Surfaced 2026-05-12 by adopter
3074    /// `docs/MIGRATION_TO_AXON.md` audit after the v1.23.0 wire-
3075    /// layer didn't honor the declarative effect. Python parser was
3076    /// fixed for the same gap 2026-05-09; this is the Rust cross-
3077    /// stack catch-up.
3078    fn parse_output_type_string(&mut self) -> Result<String, ParseError> {
3079        let expr = self.parse_type_expr()?;
3080        let mut s = expr.name;
3081        if !expr.generic_param.is_empty() {
3082            s.push('<');
3083            s.push_str(&expr.generic_param);
3084            s.push('>');
3085        }
3086        if expr.optional {
3087            s.push('?');
3088        }
3089        Ok(s)
3090    }
3091
3092    // ── FLOW ─────────────────────────────────────────────────────
3093
3094    fn parse_flow(&mut self) -> Result<FlowDefinition, ParseError> {
3095        let tok = self.consume(TokenType::Flow)?;
3096        let loc = self.loc_of(&tok);
3097        let name = self.consume(TokenType::Identifier)?.value;
3098
3099        self.consume(TokenType::LParen)?;
3100        let mut parameters = Vec::new();
3101        if !self.check(TokenType::RParen) {
3102            parameters = self.parse_param_list()?;
3103        }
3104        self.consume(TokenType::RParen)?;
3105
3106        let mut return_type = None;
3107        if self.check(TokenType::Arrow) {
3108            self.advance();
3109            return_type = Some(self.parse_type_expr()?);
3110        }
3111
3112        self.consume(TokenType::LBrace)?;
3113        let mut body = Vec::new();
3114        while !self.check(TokenType::RBrace) {
3115            body.push(self.parse_flow_step()?);
3116        }
3117        self.consume(TokenType::RBrace)?;
3118
3119        Ok(FlowDefinition {
3120            name,
3121            parameters,
3122            return_type,
3123            body,
3124            loc,
3125            leading_trivia: Vec::new(),
3126            trailing_trivia: Vec::new(),
3127        })
3128    }
3129
3130    fn parse_param_list(&mut self) -> Result<Vec<Parameter>, ParseError> {
3131        let mut params = Vec::new();
3132
3133        let name = self.consume(TokenType::Identifier)?;
3134        let ploc = self.loc_of(&name);
3135        self.consume(TokenType::Colon)?;
3136        let type_expr = self.parse_type_expr()?;
3137        params.push(Parameter {
3138            name: name.value,
3139            type_expr,
3140            loc: ploc,
3141        });
3142
3143        while self.check(TokenType::Comma) {
3144            self.advance();
3145            let name = self.consume(TokenType::Identifier)?;
3146            let ploc = self.loc_of(&name);
3147            self.consume(TokenType::Colon)?;
3148            let type_expr = self.parse_type_expr()?;
3149            params.push(Parameter {
3150                name: name.value,
3151                type_expr,
3152                loc: ploc,
3153            });
3154        }
3155        Ok(params)
3156    }
3157
3158    // ── FLOW STEP dispatch ───────────────────────────────────────
3159
3160    fn parse_flow_step(&mut self) -> Result<FlowStep, ParseError> {
3161        let tok = self.current().clone();
3162
3163        match tok.ttype {
3164            // §Fase 119.f — an epistemic block INSIDE a flow body. Its
3165            // children are hoisted to program level (see `Parser::hoisted`),
3166            // which is exactly what a top-level block already does, so the
3167            // nested spelling costs nothing downstream. The flow itself gets
3168            // no node: the block declares, it does not execute.
3169            TokenType::Know | TokenType::Believe | TokenType::Speculate
3170                if self
3171                    .tokens
3172                    .get(self.pos + 1)
3173                    .is_some_and(|t| t.ttype == TokenType::LBrace) =>
3174            {
3175                let block = self.parse_epistemic_block()?;
3176                self.hoisted.push(Declaration::Epistemic(block));
3177                self.parse_flow_step()
3178            }
3179            TokenType::Doubt
3180                if self
3181                    .tokens
3182                    .get(self.pos + 1)
3183                    .is_some_and(|t| t.ttype == TokenType::LBrace) =>
3184            {
3185                let block = self.parse_epistemic_block()?;
3186                self.hoisted.push(Declaration::Epistemic(block));
3187                self.parse_flow_step()
3188            }
3189            TokenType::Step => self.parse_step().map(FlowStep::Step),
3190            TokenType::If => self.parse_if().map(FlowStep::If),
3191            TokenType::For => self.parse_for_in().map(FlowStep::ForIn),
3192            TokenType::Let => self.parse_let().map(FlowStep::Let),
3193            TokenType::Return => self.parse_return().map(FlowStep::Return),
3194            TokenType::Break => self.parse_break().map(FlowStep::Break),
3195            TokenType::Continue => self.parse_continue().map(FlowStep::Continue),
3196            TokenType::Lambda => self.parse_lambda_data_apply().map(FlowStep::LambdaDataApply),
3197
3198            // ── Tier 2 flow steps (typed AST) ─────────────────────
3199            TokenType::Probe => self.parse_flow_step_simple("probe").map(|l| FlowStep::Probe(ProbeStep { target: l.1, fields: Vec::new(), loc: l.0 })),
3200            TokenType::Reason => self.parse_flow_step_simple("reason").map(|l| FlowStep::Reason(ReasonStep { strategy: String::new(), target: l.1, loc: l.0 })),
3201            TokenType::Validate => self.parse_flow_step_simple("validate").map(|l| FlowStep::Validate(ValidateStep { target: l.1, rule: String::new(), loc: l.0 })),
3202            TokenType::Refine => self.parse_flow_step_simple("refine").map(|l| FlowStep::Refine(RefineStep { target: l.1, strategy: String::new(), loc: l.0 })),
3203            TokenType::Weave => self.parse_weave_step(),
3204            TokenType::Use => self.parse_use_step(),
3205            TokenType::Remember => self.parse_remember_step(),
3206            TokenType::Recall => self.parse_recall_step(),
3207            TokenType::Par => self.parse_par_block().map(FlowStep::Par),
3208            TokenType::Hibernate => self.parse_hibernate_step(),
3209            TokenType::Deliberate => self.parse_block_step("deliberate").map(|l| FlowStep::Deliberate(DeliberateBlock { loc: l })),
3210            TokenType::Consensus => self.parse_block_step("consensus").map(|l| FlowStep::Consensus(ConsensusBlock { loc: l })),
3211            TokenType::Forge => self.parse_forge_step().map(FlowStep::Forge),
3212            TokenType::Focus => self.parse_focus_step(),
3213            TokenType::Grad => self.parse_grad_step(),
3214            TokenType::Associate => self.parse_associate_step(),
3215            TokenType::Aggregate => self.parse_aggregate_step(),
3216            TokenType::Explore => self.parse_explore_step(),
3217            TokenType::Ingest => self.parse_ingest_step(),
3218            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 })),
3219            // §Fase 111.e — `stream` parses its BODY. It used to go through
3220            // `parse_block_step`, whose entire job is `skip_braced_block()` —
3221            // the block's contents were thrown away at parse time, which is why
3222            // `run_stream` had nothing to run and "completed" with an empty
3223            // string while the README sold "Algebraic Effects and Free Monads".
3224            TokenType::Stream => self.parse_stream_block().map(FlowStep::Stream),
3225            TokenType::Navigate => self.parse_navigate_step(),
3226            TokenType::Drill => self.parse_drill_step(),
3227            TokenType::Trail => self.parse_flow_step_simple("trail").map(|l| FlowStep::Trail(TrailStep { navigate_ref: l.1, loc: l.0 })),
3228            TokenType::Corroborate => self.parse_corroborate_step(),
3229            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 })),
3230            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 })),
3231            // §Fase 111.f — `compute <Name> on a, b -> out`. The ARGUMENTS used to
3232            // be `Vec::new()` — hardcoded empty at the parse site — so even if
3233            // the runtime had wanted to compute something, it had nothing to
3234            // compute it FROM.
3235            TokenType::Compute => self.parse_compute_apply().map(FlowStep::ComputeApply),
3236            TokenType::Listen => self.parse_listen_step(),
3237            TokenType::Daemon => self.parse_flow_step_simple("daemon").map(|l| FlowStep::DaemonStep(DaemonStepNode { daemon_ref: l.1, loc: l.0 })),
3238            // §λ-L-E Fase 13 — Mobile typed channels (paper §3.1, §3.2, §4.3)
3239            TokenType::Emit => self.parse_emit_step(),
3240            // §Fase 92.b — `mint <Credential> as <binding>` (ephemeral credential).
3241            TokenType::Mint => self.parse_mint_step(),
3242            // §Fase 94.b — `rotate <SecretsStore> [where "…"] with <Tool> as
3243            // <binding>` (mediated secret renewal).
3244            TokenType::Rotate => self.parse_rotate_step(),
3245            TokenType::Publish => self.parse_publish_step(),
3246            TokenType::Discover => self.parse_discover_step(),
3247            TokenType::Persist => self.parse_persist_step(),
3248            TokenType::Retrieve => self.parse_retrieve_step(),
3249            TokenType::Mutate => self.parse_mutate_step(),
3250            TokenType::Purge => self.parse_store_where_step().map(|(loc, store_name, where_expr)| FlowStep::Purge(PurgeStep { store_name, where_expr, loc })),
3251            TokenType::Transact => self.parse_block_step("transact").map(|l| FlowStep::Transact(TransactBlock { loc: l })),
3252            // §Fase 88.a — the `warden` adversarial-analysis block.
3253            TokenType::Warden => self.parse_warden().map(FlowStep::Warden),
3254            // §Fase 51.a — the `quant` cognitive block (Hilbert-space projection).
3255            TokenType::Quant => self.parse_quant().map(FlowStep::Quant),
3256            // §Fase 51.d.2 — the `yield` measurement point.
3257            TokenType::Yield => self.parse_yield().map(FlowStep::Yield),
3258            // §Fase 52.c — `run <Flow>(args)` as a flow-step: invoke a declared
3259            // flow from inside a body (a `daemon` listen handler, Q3). Reuses
3260            // the top-level run parser.
3261            TokenType::Run => self.parse_run().map(FlowStep::Run),
3262
3263            _ => {
3264                // §Fase 28.e — append "Did you mean X?" hint when the
3265                // unknown token looks like a typo'd flow-body keyword
3266                // (e.g. `stepp` / `reasn` / `validte`). D3, D11.
3267                let hint = crate::smart_suggest::suggest_for(
3268                    &tok.value,
3269                    crate::smart_suggest::FLOW_BODY_KEYWORD_NAMES,
3270                );
3271                let base = format!(
3272                    "Unexpected token in flow body: '{}' — expected step, if, for, let, return, ...",
3273                    tok.value
3274                );
3275                let message = if hint.is_empty() {
3276                    base
3277                } else {
3278                    format!("{base}. {hint}")
3279                };
3280                Err(ParseError {
3281                    message,
3282                    line: tok.line,
3283                    column: tok.column,
3284                    ..Default::default()
3285                })
3286            }
3287        }
3288    }
3289
3290    // ── STEP ─────────────────────────────────────────────────────
3291
3292    fn parse_step(&mut self) -> Result<StepNode, ParseError> {
3293        let tok = self.consume(TokenType::Step)?;
3294        let loc = self.loc_of(&tok);
3295        let name = self.consume(TokenType::Identifier)?.value;
3296
3297        let mut persona_ref = String::new();
3298        if self.check(TokenType::Use) {
3299            self.advance();
3300            persona_ref = self.consume_any_ident_or_kw()?.value;
3301        }
3302
3303        self.consume(TokenType::LBrace)?;
3304
3305        let mut node = StepNode {
3306            name,
3307            persona_ref,
3308            given: String::new(),
3309            ask: String::new(),
3310            output_type: String::new(),
3311            confidence_floor: None,
3312            navigate_ref: String::new(),
3313            apply_ref: String::new(),
3314            requires_context: None,
3315            now_tz: None,
3316            guards: Vec::new(),
3317            pix_ops: Vec::new(),
3318            loc,
3319        };
3320
3321        while !self.check(TokenType::RBrace) {
3322            let inner = self.current().clone();
3323
3324            match inner.ttype {
3325                TokenType::Given => {
3326                    self.advance();
3327                    self.consume(TokenType::Colon)?;
3328                    node.given = self.parse_expression_string()?;
3329                }
3330                TokenType::Ask => {
3331                    self.advance();
3332                    self.consume(TokenType::Colon)?;
3333                    node.ask = self.consume(TokenType::StringLit)?.value;
3334                }
3335                TokenType::Output => {
3336                    // Mirror of Python `_parse_step` `case "output":`
3337                    // which uses `_parse_output_type_string` — accepts
3338                    // the FULL generic-aware shape `Stream<T>`,
3339                    // `Stream<T>?`, `Identifier?`, NOT just the bare
3340                    // head identifier. Pre-fix the step parser dropped
3341                    // `<T>` and downstream `flow_has_stream_output`'s
3342                    // `starts_with("Stream<") && ends_with('>')` then
3343                    // returned false → `implicit_transport == "json"`
3344                    // → dynamic routes served JSON instead of SSE.
3345                    self.advance();
3346                    self.consume(TokenType::Colon)?;
3347                    node.output_type = self.parse_output_type_string()?;
3348                }
3349                // §Fase 119.f — `navigate` in a step body is TWO forms, told
3350                // apart by the token after the keyword:
3351                //   `navigate: <Ref>`        the field (pre-§119.f)
3352                //   `navigate <Ref> query: …` the STATEMENT README publishes
3353                // The second is an elevation: it binds `as:` before the step
3354                // generates, so the step's `ask:` can interpolate it.
3355                TokenType::Navigate
3356                    if self
3357                        .tokens
3358                        .get(self.pos + 1)
3359                        .is_some_and(|t| t.ttype != TokenType::Colon) =>
3360                {
3361                    let op = self.parse_navigate_step()?;
3362                    node.pix_ops.push(op);
3363                }
3364                TokenType::Drill => {
3365                    let op = self.parse_drill_step()?;
3366                    node.pix_ops.push(op);
3367                }
3368                TokenType::Trail => {
3369                    let op = self
3370                        .parse_flow_step_simple("trail")
3371                        .map(|l| FlowStep::Trail(TrailStep { navigate_ref: l.1, loc: l.0 }))?;
3372                    node.pix_ops.push(op);
3373                }
3374                // §Fase 119.f — `validate <binding> against: <Schema>`, the
3375                // form README's pix family publishes inside a step. The
3376                // flow-level `validate <target>` already exists; this adds the
3377                // step position plus the `against:` clause the docs write.
3378                TokenType::Validate => {
3379                    let tok = self.current().clone();
3380                    self.advance();
3381                    let target = self.consume_any_ident_or_kw()?.value.clone();
3382                    let mut rule = String::new();
3383                    if self.current().value == "against" {
3384                        self.advance();
3385                        self.consume(TokenType::Colon)?;
3386                        rule = self.consume_any_ident_or_kw()?.value.clone();
3387                    }
3388                    node.pix_ops.push(FlowStep::Validate(ValidateStep {
3389                        target,
3390                        rule,
3391                        loc: Loc { line: tok.line, column: tok.column },
3392                    }));
3393                }
3394                TokenType::Navigate => {
3395                    self.advance();
3396                    self.consume(TokenType::Colon)?;
3397                    node.navigate_ref = self.parse_dotted_identifier()?;
3398                }
3399                TokenType::Identifier if inner.value == "confidence_floor" => {
3400                    self.advance();
3401                    self.consume(TokenType::Colon)?;
3402                    node.confidence_floor = Some(self.consume_number()?);
3403                }
3404                TokenType::Identifier if inner.value == "apply" => {
3405                    self.advance();
3406                    self.consume(TokenType::Colon)?;
3407                    node.apply_ref = self.consume_any_ident_or_kw()?.value;
3408                }
3409                // §Fase 68.b — `requires_context: <tokens>`: the step's declared
3410                // model-capability requirement (the context window the cognition
3411                // needs). A bare positive integer literal; the §68.c resolver maps
3412                // it to a concrete model. Range/ceiling is the type-checker's job
3413                // (§68.b positive-int + §68.f catalog ceiling) — the parser only
3414                // requires an integer token here (a float / non-number is a parse
3415                // error, surfaced at the exact column).
3416                TokenType::Identifier if inner.value == "requires_context" => {
3417                    self.advance();
3418                    self.consume(TokenType::Colon)?;
3419                    let num = self.current().clone();
3420                    let bad = |tok: &crate::tokens::Token| ParseError {
3421                        message: format!(
3422                            "`requires_context:` must be a positive integer token count \
3423                             (got '{}')",
3424                            tok.value
3425                        ),
3426                        line: tok.line,
3427                        column: tok.column,
3428                        ..Default::default()
3429                    };
3430                    if num.ttype != TokenType::Integer {
3431                        return Err(bad(&num));
3432                    }
3433                    let value = num.value.parse::<u32>().map_err(|_| bad(&num))?;
3434                    self.advance();
3435                    node.requires_context = Some(value);
3436                }
3437                // §Fase 91.a — `now: "<IANA-tz>"`: the step's declared cognitive
3438                // timezone. A string literal; the format law (IANA shape) is the
3439                // type-checker's job (`axon-T892`) — the parser only requires a
3440                // string token here, surfaced at the exact column.
3441                TokenType::Identifier if inner.value == "now" => {
3442                    self.advance();
3443                    self.consume(TokenType::Colon)?;
3444                    let tz = self.current().clone();
3445                    if tz.ttype != TokenType::StringLit {
3446                        return Err(ParseError {
3447                            message: format!(
3448                                "`now:` must be an IANA timezone string literal like \
3449                                 \"America/Bogota\" or \"UTC\" (got '{}')",
3450                                tz.value
3451                            ),
3452                            line: tz.line,
3453                            column: tz.column,
3454                            ..Default::default()
3455                        });
3456                    }
3457                    self.advance();
3458                    node.now_tz = Some(tz.value);
3459                }
3460                // §Fase 54.a — a `use` nested inside a `step { }` body used
3461                // to be skipped structurally (grouped with the sub-constructs
3462                // below), silently degrading the tool dispatch to an
3463                // unconstrained LLM step with NO diagnostic. That fallthrough
3464                // drops the AST node before the type-checker can see it, so the
3465                // resource the tool would provision is never linearly accounted
3466                // for (use_tool soundness). Reject it here, at the parser —
3467                // the only place that still sees the token — and redirect to
3468                // the canonical forms.
3469                TokenType::Use => {
3470                    let tool = self
3471                        .tokens
3472                        .get(self.pos + 1)
3473                        .map(|t| t.value.as_str())
3474                        .filter(|v| !v.is_empty())
3475                        .unwrap_or("<Tool>");
3476                    return Err(ParseError {
3477                        message: format!(
3478                            "`use` is not valid inside a `step {{ }}` body — the tool dispatch \
3479                             would be silently dropped. To invoke a tool, either write the \
3480                             flow-level step `use {tool} on <arg>` (outside this block), or bind \
3481                             it inside this step with `apply: {tool}`. To attach a persona, put \
3482                             it in the step header: `step <name> use <Persona> {{ … }}`."
3483                        ),
3484                        line: inner.line,
3485                        column: inner.column,
3486                        ..Default::default()
3487                    });
3488                }
3489                // §Fase 119 (D119.4) — `mandate X on Y`, `shield X on Y -> b`,
3490                // `ots X on Y` as STEP-BODY statements. README §XV has always
3491                // written the application here — next to the `output:` it
3492                // constrains — and the parser accepted the same form only at
3493                // flow level, which is why README blocks 40–42 never compiled.
3494                // The published position is also the better semantics: a
3495                // mandate inside a step is scoped to THIS step's generation;
3496                // the flow-level form governs a bare statement whose subject
3497                // must be inferred. One concept, two positions, same AST shape
3498                // as the flow-level `*ApplyStep` family.
3499                TokenType::Mandate => {
3500                    let g = self.parse_step_guard("mandate")?;
3501                    node.guards.push(g);
3502                }
3503                TokenType::Shield => {
3504                    let g = self.parse_step_guard("shield")?;
3505                    node.guards.push(g);
3506                }
3507                TokenType::Ots => {
3508                    let g = self.parse_step_guard("ots")?;
3509                    node.guards.push(g);
3510                }
3511                // §Fase 119.c — `lambda RawQuote on ticker -> verified_quote`
3512                // inside a step body: README blocks 46-47's exact shape, the
3513                // D119.4 statement position extended to the fourth member of
3514                // the apply family. Semantically it is an ELEVATION, not a
3515                // guard: dispatch runs it BEFORE the step's generation, so the
3516                // elevated binding is in scope for the prompt.
3517                TokenType::Lambda => {
3518                    let g = self.parse_step_guard("lambda")?;
3519                    node.guards.push(g);
3520                }
3521                // §Fase 119.f — `probe <target> for [a, b, c]` as a STATEMENT.
3522                //
3523                // `probe` used to fall into `skip_flow_step_structural` below,
3524                // which DISCARDED it — the §111 silent-drop shape, in the step
3525                // parser. The extraction list had nowhere to live even at flow
3526                // level. Both are fixed here: the statement is kept, and its
3527                // `for [...]` list reaches the AST.
3528                TokenType::Probe
3529                    if self
3530                        .tokens
3531                        .get(self.pos + 1)
3532                        .is_some_and(|t| t.ttype != TokenType::Colon) =>
3533                {
3534                    let tok = self.current().clone();
3535                    self.advance();
3536                    let target = self.consume_any_ident_or_kw()?.value.clone();
3537                    let mut fields = Vec::new();
3538                    if self.check(TokenType::For) {
3539                        self.advance();
3540                        self.consume(TokenType::LBracket)?;
3541                        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
3542                            fields.push(self.consume_any_ident_or_kw()?.value.clone());
3543                            if self.check(TokenType::Comma) {
3544                                self.advance();
3545                            }
3546                        }
3547                        self.consume(TokenType::RBracket)?;
3548                    }
3549                    node.pix_ops.push(FlowStep::Probe(ProbeStep {
3550                        target,
3551                        fields,
3552                        loc: Loc { line: tok.line, column: tok.column },
3553                    }));
3554                }
3555                // §Fase 119.f — `use_tool <name> [with k: v, …]` as a STATEMENT.
3556                // §54.a made `use` inside a step body a hard error pointing at
3557                // the canonical forms; `use_tool` is the OTHER spelling README
3558                // publishes, and it names the tool explicitly, so there is no
3559                // ambiguity to protect against — the dispatch is not dropped,
3560                // it is recorded.
3561                TokenType::Identifier if inner.value == "use_tool" => {
3562                    let tok = self.current().clone();
3563                    self.advance();
3564                    let tool_name = self.consume_any_ident_or_kw()?.value.clone();
3565                    let args = if self.current().value == "with" {
3566                        self.advance();
3567                        let mut named: Vec<(String, String, String)> = Vec::new();
3568                        loop {
3569                            let k = self.consume_any_ident_or_kw()?.value.clone();
3570                            self.consume(TokenType::Colon)?;
3571                            // `value_kind` mirrors §60's classification: a
3572                            // string literal is a literal, anything else is a
3573                            // binding reference the runtime must look up.
3574                            let kind = if self.check(TokenType::StringLit) {
3575                                "literal"
3576                            } else {
3577                                "reference"
3578                            };
3579                            let v = self.parse_expression_string()?;
3580                            named.push((k, v, kind.to_string()));
3581                            if self.check(TokenType::Comma) {
3582                                self.advance();
3583                            } else {
3584                                break;
3585                            }
3586                        }
3587                        UseArgs::Named(named)
3588                    } else if self.current().value == "on" {
3589                        self.advance();
3590                        UseArgs::LegacyPositional(
3591                            self.consume_any_ident_or_kw()?.value.clone(),
3592                        )
3593                    } else {
3594                        UseArgs::LegacyPositional(String::new())
3595                    };
3596                    node.pix_ops.push(FlowStep::UseTool(UseToolStep {
3597                        tool_name,
3598                        args,
3599                        loc: Loc { line: tok.line, column: tok.column },
3600                    }));
3601                }
3602                // §Fase 119.f — `par { … }` inside a step body.
3603                TokenType::Par => {
3604                    let block = self.parse_par_block()?;
3605                    node.pix_ops.push(FlowStep::Par(block));
3606                }
3607                // Sub-constructs (reason, weave, stream) → skip structurally.
3608                //
3609                // ⚠️ §Fase 119.f — this arm is still the §111 silent-drop shape
3610                // for the three that remain: a `reason { … }` / `weave [ … ]` /
3611                // `stream { … }` written in a step body is PARSED AND THROWN
3612                // AWAY. `probe` left it above; the other three need their own
3613                // AST slots on the step, which is a wider change than the
3614                // README ledger needs today. Named here so it is not
3615                // rediscovered as a surprise.
3616                TokenType::Probe
3617                | TokenType::Reason
3618                | TokenType::Weave
3619                | TokenType::Stream => {
3620                    self.skip_flow_step_structural()?;
3621                }
3622                _ => {
3623                    return Err(ParseError {
3624                        message: format!(
3625                            "Unexpected token in step body: '{}' — expected given, ask, \
3626                             probe, reason, weave, stream, output, confidence_floor, navigate, \
3627                             apply, requires_context, now",
3628                            inner.value
3629                        ),
3630                        line: inner.line,
3631                        column: inner.column,
3632                                            ..Default::default()
3633                    });
3634                }
3635            }
3636        }
3637        self.consume(TokenType::RBrace)?;
3638        Ok(node)
3639    }
3640
3641    /// Skip a flow-level sub-construct structurally (consume keyword + args + optional block).
3642    fn skip_flow_step_structural(&mut self) -> Result<(), ParseError> {
3643        // Consume the keyword
3644        self.advance();
3645        // Consume tokens until we hit a { or a closing }, or a known flow step keyword
3646        while !self.check(TokenType::LBrace)
3647            && !self.check(TokenType::RBrace)
3648            && !self.check(TokenType::Eof)
3649        {
3650            // Check if we hit a new step-level keyword (means this was a one-liner)
3651            let tt = &self.current().ttype;
3652            if matches!(
3653                tt,
3654                TokenType::Step
3655                    | TokenType::Given
3656                    | TokenType::Ask
3657                    | TokenType::Output
3658                    | TokenType::Navigate
3659                    | TokenType::Use
3660                    | TokenType::Probe
3661                    | TokenType::Reason
3662                    | TokenType::Weave
3663                    | TokenType::Stream
3664                    | TokenType::If
3665                    | TokenType::For
3666                    | TokenType::Let
3667                    | TokenType::Return
3668            ) {
3669                return Ok(());
3670            }
3671            self.advance();
3672        }
3673        // If block, skip it
3674        if self.check(TokenType::LBrace) {
3675            self.skip_braced_block()?;
3676        }
3677        Ok(())
3678    }
3679
3680    // ── INTENT ───────────────────────────────────────────────────
3681
3682    fn parse_intent(&mut self) -> Result<IntentNode, ParseError> {
3683        let tok = self.consume(TokenType::Intent)?;
3684        let loc = self.loc_of(&tok);
3685        let name = self.consume(TokenType::Identifier)?.value;
3686        self.consume(TokenType::LBrace)?;
3687
3688        let mut node = IntentNode {
3689            name,
3690            given: String::new(),
3691            ask: String::new(),
3692            output_type: None,
3693            confidence_floor: None,
3694            loc,
3695            leading_trivia: Vec::new(),
3696            trailing_trivia: Vec::new(),
3697        };
3698
3699        while !self.check(TokenType::RBrace) {
3700            let field_name = self.current().value.clone();
3701            self.advance();
3702            self.consume(TokenType::Colon)?;
3703
3704            match field_name.as_str() {
3705                "given" => node.given = self.consume(TokenType::Identifier)?.value,
3706                "ask" => node.ask = self.consume(TokenType::StringLit)?.value,
3707                "output" => node.output_type = Some(self.parse_type_expr()?),
3708                "confidence_floor" => node.confidence_floor = Some(self.consume_number()?),
3709                _ => self.skip_value(),
3710            }
3711        }
3712        self.consume(TokenType::RBrace)?;
3713        Ok(node)
3714    }
3715
3716    // ── RUN ──────────────────────────────────────────────────────
3717
3718    fn parse_run(&mut self) -> Result<RunStatement, ParseError> {
3719        let tok = self.consume(TokenType::Run)?;
3720        let loc = self.loc_of(&tok);
3721        let flow_name = self.consume(TokenType::Identifier)?.value;
3722
3723        self.consume(TokenType::LParen)?;
3724        let mut arguments = Vec::new();
3725        if !self.check(TokenType::RParen) {
3726            arguments = self.parse_argument_list()?;
3727        }
3728        self.consume(TokenType::RParen)?;
3729
3730        let mut node = RunStatement {
3731            flow_name,
3732            arguments,
3733            persona: String::new(),
3734            context: String::new(),
3735            anchors: Vec::new(),
3736            on_failure: String::new(),
3737            on_failure_params: Vec::new(),
3738            output_to: String::new(),
3739            effort: String::new(),
3740            loc,
3741            leading_trivia: Vec::new(),
3742            trailing_trivia: Vec::new(),
3743        };
3744
3745        while self.check_run_modifier() {
3746            let mod_tok = self.current().clone();
3747            match mod_tok.ttype {
3748                TokenType::As => {
3749                    self.advance();
3750                    node.persona = self.consume(TokenType::Identifier)?.value;
3751                }
3752                TokenType::Within => {
3753                    self.advance();
3754                    node.context = self.consume(TokenType::Identifier)?.value;
3755                }
3756                TokenType::ConstrainedBy => {
3757                    self.advance();
3758                    node.anchors = self.parse_bracketed_identifiers()?;
3759                }
3760                TokenType::OnFailure => {
3761                    self.advance();
3762                    self.consume(TokenType::Colon)?;
3763                    node.on_failure = self.consume_any_ident_or_kw()?.value;
3764                    // Parse optional params: (key: val, ...)
3765                    if self.check(TokenType::LParen) {
3766                        self.advance();
3767                        while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
3768                            let key = self.consume_any_ident_or_kw()?.value;
3769                            self.consume(TokenType::Colon)?;
3770                            let val = self.consume_any_ident_or_kw()?.value;
3771                            node.on_failure_params.push((key, val));
3772                            if self.check(TokenType::Comma) {
3773                                self.advance();
3774                            }
3775                        }
3776                        if self.check(TokenType::RParen) {
3777                            self.advance();
3778                        }
3779                    }
3780                }
3781                TokenType::OutputTo => {
3782                    self.advance();
3783                    self.consume(TokenType::Colon)?;
3784                    node.output_to = self.consume(TokenType::StringLit)?.value;
3785                }
3786                TokenType::Effort => {
3787                    self.advance();
3788                    self.consume(TokenType::Colon)?;
3789                    node.effort = self.consume_any_ident_or_kw()?.value;
3790                }
3791                _ => break,
3792            }
3793        }
3794
3795        Ok(node)
3796    }
3797
3798    // ── EPISTEMIC BLOCK ──────────────────────────────────────────
3799
3800    fn parse_epistemic_block(&mut self) -> Result<EpistemicBlock, ParseError> {
3801        let tok = self.current().clone();
3802        let mode = match tok.ttype {
3803            TokenType::Know => "know",
3804            TokenType::Believe => "believe",
3805            TokenType::Speculate => "speculate",
3806            TokenType::Doubt => "doubt",
3807            _ => unreachable!(),
3808        };
3809        self.advance();
3810        let loc = self.loc_of(&tok);
3811
3812        self.consume(TokenType::LBrace)?;
3813        let mut body = Vec::new();
3814        while !self.check(TokenType::RBrace) {
3815            body.push(self.parse_declaration()?);
3816        }
3817        self.consume(TokenType::RBrace)?;
3818
3819        Ok(EpistemicBlock {
3820            mode: mode.to_string(),
3821            body,
3822            loc,
3823            leading_trivia: Vec::new(),
3824            trailing_trivia: Vec::new(),
3825        })
3826    }
3827
3828    // ── IF ────────────────────────────────────────────────────────
3829
3830    // ── §Fase 70.a — the pure expression engine (Pratt parser) ───────────
3831
3832    /// Parse a pure expression (§Fase 70). Precedence-climbing: `or` < `and` <
3833    /// comparison < `+ -` < `* / %` < unary (`- not`) < atom. Total + pure; no
3834    /// side effects. Field/index access + the builtin catalog land in §70.c/d.
3835    fn parse_expr(&mut self) -> Result<Expr, ParseError> {
3836        self.parse_expr_bp(0)
3837    }
3838
3839    fn parse_expr_bp(&mut self, min_bp: u8) -> Result<Expr, ParseError> {
3840        // Prefix: unary `-` (negation) / `not` (boolean). Binds tighter than
3841        // every binary operator (bp 6).
3842        let mut lhs = match self.current().ttype {
3843            TokenType::Minus => {
3844                self.advance();
3845                Expr::Unary(UnOp::Neg, Box::new(self.parse_expr_bp(6)?))
3846            }
3847            TokenType::Not => {
3848                self.advance();
3849                Expr::Unary(UnOp::Not, Box::new(self.parse_expr_bp(6)?))
3850            }
3851            _ => self.parse_postfix()?,
3852        };
3853        // Infix: left-associative (right_bp = left_bp + 1).
3854        while let Some((op, lbp)) = Self::binop_of(self.current().ttype.clone()) {
3855            if lbp < min_bp {
3856                break;
3857            }
3858            self.advance();
3859            let rhs = self.parse_expr_bp(lbp + 1)?;
3860            lhs = Expr::Binary(op, Box::new(lhs), Box::new(rhs));
3861        }
3862        Ok(lhs)
3863    }
3864
3865    /// Map a token to `(BinOp, left binding power)`, or `None` if it is not an
3866    /// infix operator (which stops the climb — e.g. at `->` or `{`).
3867    fn binop_of(t: TokenType) -> Option<(BinOp, u8)> {
3868        Some(match t {
3869            TokenType::Or => (BinOp::Or, 1),
3870            TokenType::And => (BinOp::And, 2),
3871            TokenType::Eq => (BinOp::Eq, 3),
3872            TokenType::Neq => (BinOp::Ne, 3),
3873            TokenType::Lt => (BinOp::Lt, 3),
3874            TokenType::Lte => (BinOp::Le, 3),
3875            TokenType::Gt => (BinOp::Gt, 3),
3876            TokenType::Gte => (BinOp::Ge, 3),
3877            TokenType::Plus => (BinOp::Add, 4),
3878            TokenType::Minus => (BinOp::Sub, 4),
3879            TokenType::Star => (BinOp::Mul, 5),
3880            TokenType::Slash => (BinOp::Div, 5),
3881            TokenType::Percent => (BinOp::Mod, 5),
3882            _ => return None,
3883        })
3884    }
3885
3886    /// §Fase 70.c — parse a primary then its `.` postfix chain: a builtin call
3887    /// (`.length`, `.contains(x)`) when the name is in the closed catalog, else
3888    /// a dotted reference-path continuation (`a.b.c` → `Ref("a.b.c")`, the
3889    /// pre-§70.c behaviour). Field access on a non-reference (`(a+b).x`) is
3890    /// reserved for §70.d.
3891    fn parse_postfix(&mut self) -> Result<Expr, ParseError> {
3892        let mut expr = self.parse_expr_atom()?;
3893        loop {
3894            if self.check(TokenType::Dot) {
3895                self.advance();
3896                let name = self.consume_any_ident_or_kw()?.value;
3897                if let Some(builtin) = Builtin::from_name(&name) {
3898                    let mut args = vec![expr];
3899                    if self.check(TokenType::LParen) {
3900                        self.advance();
3901                        while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
3902                            args.push(self.parse_expr_bp(0)?);
3903                            if self.check(TokenType::Comma) {
3904                                self.advance();
3905                            } else {
3906                                break;
3907                            }
3908                        }
3909                        self.consume(TokenType::RParen)?;
3910                    }
3911                    expr = Expr::Call(builtin, args);
3912                } else {
3913                    // §Fase 70.d — a plain dotted path on a Ref extends the Ref
3914                    // (back-compat: `a.b.c` → `Ref("a.b.c")`); on any other base
3915                    // it is a structured field access (the JSONB seam).
3916                    expr = match expr {
3917                        Expr::Ref(p) => Expr::Ref(format!("{p}.{name}")),
3918                        other => Expr::Field(Box::new(other), name),
3919                    };
3920                }
3921            } else if self.check(TokenType::LBracket) {
3922                // §Fase 70.d — index access `base[index]`.
3923                self.advance();
3924                let index = self.parse_expr_bp(0)?;
3925                self.consume(TokenType::RBracket)?;
3926                expr = Expr::Index(Box::new(expr), Box::new(index));
3927            } else {
3928                break;
3929            }
3930        }
3931        Ok(expr)
3932    }
3933
3934    fn parse_expr_atom(&mut self) -> Result<Expr, ParseError> {
3935        let tok = self.current().clone();
3936        match tok.ttype {
3937            TokenType::Integer => {
3938                self.advance();
3939                let lit = tok
3940                    .value
3941                    .parse::<i64>()
3942                    .map(ExprLit::Int)
3943                    .or_else(|_| tok.value.parse::<f64>().map(ExprLit::Float))
3944                    .map_err(|_| ParseError {
3945                        message: format!("invalid integer literal '{}'", tok.value),
3946                        line: tok.line,
3947                        column: tok.column,
3948                        ..Default::default()
3949                    })?;
3950                Ok(Expr::Lit(lit))
3951            }
3952            TokenType::Float => {
3953                self.advance();
3954                let f = tok.value.parse::<f64>().map_err(|_| ParseError {
3955                    message: format!("invalid float literal '{}'", tok.value),
3956                    line: tok.line,
3957                    column: tok.column,
3958                    ..Default::default()
3959                })?;
3960                Ok(Expr::Lit(ExprLit::Float(f)))
3961            }
3962            TokenType::Bool => {
3963                self.advance();
3964                Ok(Expr::Lit(ExprLit::Bool(tok.value == "true")))
3965            }
3966            TokenType::StringLit => {
3967                self.advance();
3968                Ok(Expr::Lit(ExprLit::Str(tok.value)))
3969            }
3970            TokenType::LParen => {
3971                self.advance();
3972                let inner = self.parse_expr_bp(0)?;
3973                self.consume(TokenType::RParen)?;
3974                Ok(inner)
3975            }
3976            _ => {
3977                // Reference: a single identifier (or keyword used as a name).
3978                // The `.` chain (dotted path / builtin call) is handled by the
3979                // postfix layer (§70.c `parse_postfix`).
3980                Ok(Expr::Ref(self.consume_any_ident_or_kw()?.value))
3981            }
3982        }
3983    }
3984
3985    /// §Fase 70.a — render a literal to its legacy surface string (for the
3986    /// back-compat `(condition, op, value)` triple). Only used when an
3987    /// expression fits the legacy shape; numeric round-tripping is exact for
3988    /// ints and faithful-enough for floats (the legacy runtime re-parses it).
3989    fn expr_lit_surface(lit: &ExprLit) -> String {
3990        match lit {
3991            ExprLit::Int(i) => i.to_string(),
3992            ExprLit::Float(f) => f.to_string(),
3993            ExprLit::Bool(b) => b.to_string(),
3994            ExprLit::Str(s) => s.clone(),
3995        }
3996    }
3997
3998    fn expr_leaf_surface(expr: &Expr) -> Option<String> {
3999        match expr {
4000            Expr::Ref(p) => Some(p.clone()),
4001            Expr::Lit(l) => Some(Self::expr_lit_surface(l)),
4002            _ => None,
4003        }
4004    }
4005
4006    /// A legacy "leaf" is a bare reference (truthy check) or a
4007    /// `<ref> <cmp> <ref|literal>` triple — exactly what the pre-§70 `if`
4008    /// grammar could express.
4009    fn expr_legacy_leaf(expr: &Expr) -> Option<(String, String, String)> {
4010        match expr {
4011            Expr::Ref(p) => Some((p.clone(), String::new(), String::new())),
4012            Expr::Binary(op, l, r) => {
4013                let op_s = match op {
4014                    BinOp::Eq => "==",
4015                    BinOp::Ne => "!=",
4016                    BinOp::Lt => "<",
4017                    BinOp::Le => "<=",
4018                    BinOp::Gt => ">",
4019                    BinOp::Ge => ">=",
4020                    _ => return None,
4021                };
4022                let lhs = match &**l {
4023                    Expr::Ref(p) => p.clone(),
4024                    _ => return None,
4025                };
4026                let rhs = Self::expr_leaf_surface(r)?;
4027                Some((lhs, op_s.to_string(), rhs))
4028            }
4029            _ => None,
4030        }
4031    }
4032
4033    /// Flatten an `or`-tree of legacy leaves in left-to-right order. Returns
4034    /// `false` (and leaves `out` unusable) if any node is not a legacy leaf.
4035    fn collect_or_leaves(expr: &Expr, out: &mut Vec<(String, String, String)>) -> bool {
4036        match expr {
4037            Expr::Binary(BinOp::Or, l, r) => {
4038                Self::collect_or_leaves(l, out) && Self::collect_or_leaves(r, out)
4039            }
4040            _ => match Self::expr_legacy_leaf(expr) {
4041                Some(t) => {
4042                    out.push(t);
4043                    true
4044                }
4045                None => false,
4046            },
4047        }
4048    }
4049
4050    /// §Fase 70.a — if the parsed condition fits the legacy
4051    /// `(condition, op, value)` + `or`-chain shape, return the legacy fields so
4052    /// the IR + runtime stay byte-identical to pre-§70 (zero drift). `None` ⇒
4053    /// the condition uses richer forms (`and`, `not`, arithmetic, parentheses,
4054    /// nesting) and must ride the `cond` expression evaluator.
4055    #[allow(clippy::type_complexity)]
4056    fn cond_as_legacy(
4057        expr: &Expr,
4058    ) -> Option<(String, String, String, Vec<(String, String, String)>, String)> {
4059        let mut leaves = Vec::new();
4060        if !Self::collect_or_leaves(expr, &mut leaves) || leaves.is_empty() {
4061            return None;
4062        }
4063        let (c0, o0, v0) = leaves[0].clone();
4064        let rest = leaves[1..].to_vec();
4065        let conjunctor = if rest.is_empty() {
4066            String::new()
4067        } else {
4068            "or".to_string()
4069        };
4070        Some((c0, o0, v0, rest, conjunctor))
4071    }
4072
4073    fn parse_if(&mut self) -> Result<ConditionalNode, ParseError> {
4074        let tok = self.consume(TokenType::If)?;
4075        let loc = self.loc_of(&tok);
4076
4077        // §Fase 70.a — parse the condition as a pure expression, then split:
4078        // a legacy-expressible condition populates the legacy triple fields
4079        // (cond = None → byte-identical IR + eval); a richer condition rides
4080        // the `cond` expression evaluator.
4081        let expr = self.parse_expr()?;
4082        let (condition, comparison_op, comparison_value, conditions, conjunctor, cond) =
4083            match Self::cond_as_legacy(&expr) {
4084                Some((c, o, v, more, conj)) => (c, o, v, more, conj, None),
4085                None => (
4086                    String::new(),
4087                    String::new(),
4088                    String::new(),
4089                    Vec::new(),
4090                    String::new(),
4091                    Some(expr),
4092                ),
4093            };
4094
4095        let mut then_body = Vec::new();
4096        let mut else_body = Vec::new();
4097
4098        // Arrow form or block form
4099        if self.check(TokenType::Arrow) {
4100            self.advance();
4101            then_body.push(self.parse_flow_step()?);
4102        } else if self.check(TokenType::LBrace) {
4103            self.advance();
4104            while !self.check(TokenType::RBrace) {
4105                then_body.push(self.parse_flow_step()?);
4106            }
4107            self.consume(TokenType::RBrace)?;
4108        }
4109
4110        // Else branch
4111        if self.check(TokenType::Else) {
4112            self.advance();
4113            if self.check(TokenType::Arrow) {
4114                self.advance();
4115                else_body.push(self.parse_flow_step()?);
4116            } else if self.check(TokenType::LBrace) {
4117                self.advance();
4118                while !self.check(TokenType::RBrace) {
4119                    else_body.push(self.parse_flow_step()?);
4120                }
4121                self.consume(TokenType::RBrace)?;
4122            }
4123        }
4124
4125        Ok(ConditionalNode {
4126            condition,
4127            comparison_op,
4128            comparison_value,
4129            then_body,
4130            else_body,
4131            conditions,
4132            conjunctor,
4133            cond,
4134            loc,
4135        })
4136    }
4137
4138    // ── FOR IN ───────────────────────────────────────────────────
4139
4140    fn parse_for_in(&mut self) -> Result<ForInStatement, ParseError> {
4141        let tok = self.consume(TokenType::For)?;
4142        let loc = self.loc_of(&tok);
4143        let variable = self.consume(TokenType::Identifier)?.value;
4144        self.consume(TokenType::In)?;
4145        let iterable = self.parse_dotted_identifier()?;
4146
4147        self.consume(TokenType::LBrace)?;
4148        // Fase 19.e — increment loop_depth so `parse_break` /
4149        // `parse_continue` inside the body pass the scope check.
4150        // Decrement on every exit path (Ok / Err) so a parse error
4151        // mid-body does not leave the depth permanently elevated
4152        // for later top-level parsing — `?` would skip the
4153        // decrement otherwise.
4154        self.loop_depth += 1;
4155        let body_result = (|| -> Result<Vec<FlowStep>, ParseError> {
4156            let mut body = Vec::new();
4157            while !self.check(TokenType::RBrace) {
4158                body.push(self.parse_flow_step()?);
4159            }
4160            Ok(body)
4161        })();
4162        self.loop_depth -= 1;
4163        let body = body_result?;
4164        self.consume(TokenType::RBrace)?;
4165
4166        Ok(ForInStatement {
4167            variable,
4168            iterable,
4169            body,
4170            loc,
4171        })
4172    }
4173
4174    /// Fase 19.e — `break` keyword. Compile-time scope check
4175    /// (`loop_depth == 0`) rejects break outside a for-in body.
4176    fn parse_break(&mut self) -> Result<BreakStatement, ParseError> {
4177        let tok = self.consume(TokenType::Break)?;
4178        let loc = self.loc_of(&tok);
4179        if self.loop_depth == 0 {
4180            return Err(ParseError {
4181                message: "'break' outside of a for-in loop body".to_string(),
4182                line: tok.line,
4183                column: tok.column,
4184                            ..Default::default()
4185            });
4186        }
4187        Ok(BreakStatement { loc })
4188    }
4189
4190    /// Fase 19.e — `continue` keyword. Same scope check as
4191    /// `parse_break`.
4192    fn parse_continue(&mut self) -> Result<ContinueStatement, ParseError> {
4193        let tok = self.consume(TokenType::Continue)?;
4194        let loc = self.loc_of(&tok);
4195        if self.loop_depth == 0 {
4196            return Err(ParseError {
4197                message: "'continue' outside of a for-in loop body".to_string(),
4198                line: tok.line,
4199                column: tok.column,
4200                            ..Default::default()
4201            });
4202        }
4203        Ok(ContinueStatement { loc })
4204    }
4205
4206    // ── LET ──────────────────────────────────────────────────────
4207
4208    fn parse_let(&mut self) -> Result<LetStatement, ParseError> {
4209        let tok = self.consume(TokenType::Let)?;
4210        let loc = self.loc_of(&tok);
4211
4212        // Name can be an identifier or a keyword used as binding name
4213        let name = self.consume_any_ident_or_kw()?.value;
4214        // §Fase 51.c.3 — optional type annotation `let x: <TypeExpr> = …`.
4215        let type_annotation = if self.check(TokenType::Colon) {
4216            self.advance();
4217            Some(self.parse_type_expr()?)
4218        } else {
4219            None
4220        };
4221        self.consume(TokenType::Assign)?;
4222        // Fase 17.a — reset side-channel before parsing value; the
4223        // atom / expr helpers tag the kind as they descend.
4224        self.last_let_value_kind = "literal".to_string();
4225        let (value, value_ast) = self.parse_let_value_expr_with_ast()?;
4226
4227        Ok(LetStatement {
4228            identifier: name,
4229            value_expr: value,
4230            value_kind: self.last_let_value_kind.clone(),
4231            type_annotation,
4232            value_ast,
4233            loc,
4234            leading_trivia: Vec::new(),
4235            trailing_trivia: Vec::new(),
4236        })
4237    }
4238
4239    fn parse_let_value_expr(&mut self) -> Result<String, ParseError> {
4240        let atom = self.parse_let_atom()?;
4241
4242        // Arithmetic expression: collect as string
4243        if matches!(
4244            self.current().ttype,
4245            TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
4246        ) {
4247            let mut parts = vec![atom];
4248            while matches!(
4249                self.current().ttype,
4250                TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
4251            ) {
4252                parts.push(self.advance().value.clone());
4253                parts.push(self.parse_let_atom()?);
4254            }
4255            self.last_let_value_kind = "expression".to_string();
4256            return Ok(parts.join(" "));
4257        }
4258        Ok(atom)
4259    }
4260
4261    /// §Fase 70.f — parse a `let`-binding value, additionally producing a
4262    /// structured `value_ast` for the expression case. A list literal keeps the
4263    /// dedicated path; everything else is parsed through the §70 expression
4264    /// engine and classified: a bare literal / reference keeps its pre-§70
4265    /// string form (`value_ast = None`, byte-identical), while a real expression
4266    /// (`price * qty`, `recent.length`) additionally carries a `value_ast` the
4267    /// runtime evaluates for real (pre-§70.f it was treated as an opaque literal
4268    /// string). Used ONLY by `parse_let` — other value positions (list items,
4269    /// remember/stream values) keep the string-only `parse_let_value_expr`.
4270    fn parse_let_value_expr_with_ast(&mut self) -> Result<(String, Option<Expr>), ParseError> {
4271        if self.check(TokenType::LBracket) {
4272            self.last_let_value_kind = "literal".to_string();
4273            return Ok((self.parse_let_list_literal()?, None));
4274        }
4275        let expr = self.parse_expr()?;
4276        Ok(match expr {
4277            Expr::Lit(lit) => {
4278                self.last_let_value_kind = "literal".to_string();
4279                (Self::expr_lit_surface(&lit), None)
4280            }
4281            Expr::Ref(p) => {
4282                self.last_let_value_kind = "reference".to_string();
4283                (p, None)
4284            }
4285            other => {
4286                self.last_let_value_kind = "expression".to_string();
4287                (Self::render_expr(&other), Some(other))
4288            }
4289        })
4290    }
4291
4292    /// §Fase 70.f — a readable surface rendering of an expression for the
4293    /// vestigial `value_expr` string (the runtime uses `value_ast`).
4294    fn render_expr(e: &Expr) -> String {
4295        match e {
4296            Expr::Lit(l) => Self::expr_lit_surface(l),
4297            Expr::Ref(p) => p.clone(),
4298            Expr::Unary(UnOp::Neg, x) => format!("-{}", Self::render_expr(x)),
4299            Expr::Unary(UnOp::Not, x) => format!("not {}", Self::render_expr(x)),
4300            Expr::Binary(op, l, r) => {
4301                let sym = match op {
4302                    BinOp::Add => "+",
4303                    BinOp::Sub => "-",
4304                    BinOp::Mul => "*",
4305                    BinOp::Div => "/",
4306                    BinOp::Mod => "%",
4307                    BinOp::Eq => "==",
4308                    BinOp::Ne => "!=",
4309                    BinOp::Lt => "<",
4310                    BinOp::Le => "<=",
4311                    BinOp::Gt => ">",
4312                    BinOp::Ge => ">=",
4313                    BinOp::And => "and",
4314                    BinOp::Or => "or",
4315                };
4316                format!("({} {sym} {})", Self::render_expr(l), Self::render_expr(r))
4317            }
4318            Expr::Call(b, args) => {
4319                let recv = args.first().map(Self::render_expr).unwrap_or_default();
4320                let rest: Vec<String> = args.iter().skip(1).map(Self::render_expr).collect();
4321                if rest.is_empty() {
4322                    format!("{recv}.{}", b.surface())
4323                } else {
4324                    format!("{recv}.{}({})", b.surface(), rest.join(", "))
4325                }
4326            }
4327            Expr::Field(b, f) => format!("{}.{f}", Self::render_expr(b)),
4328            Expr::Index(b, i) => format!("{}[{}]", Self::render_expr(b), Self::render_expr(i)),
4329        }
4330    }
4331
4332    fn parse_let_atom(&mut self) -> Result<String, ParseError> {
4333        let tok = self.current().clone();
4334
4335        match tok.ttype {
4336            TokenType::StringLit => {
4337                self.last_let_value_kind = "literal".to_string();
4338                self.advance();
4339                Ok(tok.value)
4340            }
4341            TokenType::Integer | TokenType::Float => {
4342                self.last_let_value_kind = "literal".to_string();
4343                self.advance();
4344                Ok(tok.value)
4345            }
4346            TokenType::Bool => {
4347                self.last_let_value_kind = "literal".to_string();
4348                self.advance();
4349                Ok(tok.value)
4350            }
4351            TokenType::Identifier => {
4352                self.last_let_value_kind = "reference".to_string();
4353                self.parse_dotted_identifier()
4354            }
4355            TokenType::LBracket => {
4356                self.last_let_value_kind = "literal".to_string();
4357                self.parse_let_list_literal()
4358            }
4359            _ => {
4360                // Keywords starting a dotted path (pix.document_tree)
4361                if self.pos + 1 < self.tokens.len()
4362                    && self.tokens[self.pos + 1].ttype == TokenType::Dot
4363                {
4364                    self.last_let_value_kind = "reference".to_string();
4365                    return self.parse_dotted_identifier();
4366                }
4367                Err(ParseError {
4368                    message: format!(
4369                        "Expected value expression, found {:?}('{}')",
4370                        tok.ttype, tok.value
4371                    ),
4372                    line: tok.line,
4373                    column: tok.column,
4374                                    ..Default::default()
4375                })
4376            }
4377        }
4378    }
4379
4380    fn parse_let_list_literal(&mut self) -> Result<String, ParseError> {
4381        self.consume(TokenType::LBracket)?;
4382        let mut items = Vec::new();
4383        if !self.check(TokenType::RBracket) {
4384            items.push(self.parse_let_value_expr()?);
4385            while self.check(TokenType::Comma) {
4386                self.advance();
4387                if self.check(TokenType::RBracket) {
4388                    break; // trailing comma
4389                }
4390                items.push(self.parse_let_value_expr()?);
4391            }
4392        }
4393        self.consume(TokenType::RBracket)?;
4394        Ok(format!("[{}]", items.join(", ")))
4395    }
4396
4397    // ── RETURN ───────────────────────────────────────────────────
4398
4399    fn parse_return(&mut self) -> Result<ReturnStatement, ParseError> {
4400        let tok = self.consume(TokenType::Return)?;
4401        let loc = self.loc_of(&tok);
4402        let value = self.parse_let_value_expr()?;
4403        Ok(ReturnStatement {
4404            value_expr: value,
4405            loc,
4406        })
4407    }
4408
4409    // ── TIER 2 FLOW STEP HELPERS ────────────────────────────────────
4410
4411    /// Parse: keyword target (consumes keyword + one identifier/keyword-as-value).
4412    fn parse_flow_step_simple(&mut self, _kw: &str) -> Result<(Loc, String), ParseError> {
4413        let tok = self.current().clone();
4414        self.advance(); // consume keyword
4415        let target = if self.at_declaration_start()
4416            || self.check(TokenType::RBrace)
4417            || self.check(TokenType::Eof)
4418        {
4419            String::new()
4420        } else {
4421            self.consume_any_ident_or_kw()?.value.clone()
4422        };
4423        // Skip optional braced block
4424        if self.check(TokenType::LBrace) {
4425            self.skip_braced_block()?;
4426        }
4427        Ok((
4428            Loc {
4429                line: tok.line,
4430                column: tok.column,
4431            },
4432            target,
4433        ))
4434    }
4435
4436    /// Parse: keyword { ... } — block-level step, skip body structurally.
4437    /// §Fase 111.e — `stream { <steps> }` with a REAL body.
4438    ///
4439    /// The four block primitives (`deliberate`, `consensus`, `stream`,
4440    /// `transact`) all went through [`Self::parse_block_step`], whose entire job
4441    /// is `skip_braced_block()`. Their bodies were discarded at parse time — so
4442    /// their handlers were not no-ops through neglect, they were no-ops
4443    /// *by construction*: there was nothing in the AST to execute. §111 retracted
4444    /// `transact`; this gives `stream` its body back. `deliberate` / `consensus`
4445    /// remain body-less pending their Tier-4 disposition.
4446    fn parse_stream_block(&mut self) -> Result<StreamBlock, ParseError> {
4447        let tok = self.current().clone();
4448        let loc = self.loc_of(&tok);
4449        self.advance(); // consume `stream`
4450
4451        // Tolerate the pre-111 form `stream <effect-ish tokens> { … }`: skip any
4452        // argument tokens ahead of the brace, exactly as `parse_block_step` did,
4453        // so an existing program keeps parsing. Only the BODY changes.
4454        while !self.check(TokenType::LBrace)
4455            && !self.check(TokenType::RBrace)
4456            && !self.check(TokenType::Eof)
4457            && !self.at_declaration_start()
4458        {
4459            self.advance();
4460        }
4461
4462        let mut body = Vec::new();
4463        if self.check(TokenType::LBrace) {
4464            self.advance();
4465            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4466                body.push(self.parse_flow_step()?);
4467            }
4468            self.consume(TokenType::RBrace)?;
4469        }
4470
4471        Ok(StreamBlock { body, loc })
4472    }
4473
4474    fn parse_block_step(&mut self, _kw: &str) -> Result<Loc, ParseError> {
4475        let tok = self.current().clone();
4476        self.advance();
4477        // Skip optional arguments before brace
4478        while !self.check(TokenType::LBrace)
4479            && !self.check(TokenType::RBrace)
4480            && !self.check(TokenType::Eof)
4481            && !self.at_declaration_start()
4482        {
4483            self.advance();
4484        }
4485        if self.check(TokenType::LBrace) {
4486            self.skip_braced_block()?;
4487        }
4488        Ok(Loc {
4489            line: tok.line,
4490            column: tok.column,
4491        })
4492    }
4493
4494    /// §Fase 86 — parse `forge <Name>(seed: "<text>") -> <Type> { mode:,
4495    /// novelty:, depth:, branches:, constraints: }`. Real field capture
4496    /// (replacing the pre-§86 discard-everything stub). Strict closed-catalog:
4497    /// an unknown field is a hard parse error; all cross-field laws (Boden mode
4498    /// catalog, novelty range, depth/branches ≥ 1, `constraints:` → `anchor`)
4499    /// are §86.c type-checker territory.
4500    fn parse_forge_step(&mut self) -> Result<ForgeBlock, ParseError> {
4501        let tok = self.consume(TokenType::Forge)?;
4502        let name = self.consume(TokenType::Identifier)?.value;
4503        let mut node = ForgeBlock {
4504            name,
4505            novelty: 0.5,
4506            depth: 1,
4507            branches: 1,
4508            loc: Loc { line: tok.line, column: tok.column },
4509            ..Default::default()
4510        };
4511        // `(seed: "...")`
4512        self.consume(TokenType::LParen)?;
4513        let arg = self.consume_any_ident_or_kw()?.value;
4514        self.consume(TokenType::Colon)?;
4515        if arg != "seed" {
4516            return Err(self.error(&format!(
4517                "forge '{}' expects `seed:` as its argument, found `{arg}`",
4518                node.name
4519            )));
4520        }
4521        node.seed = self.consume(TokenType::StringLit)?.value;
4522        self.consume(TokenType::RParen)?;
4523        // `-> <Type>`
4524        self.consume(TokenType::Arrow)?;
4525        node.output_type = self.consume_any_ident_or_kw()?.value;
4526        // `{ fields }`
4527        self.consume(TokenType::LBrace)?;
4528        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4529            let field = self.consume_any_ident_or_kw()?.value;
4530            self.consume(TokenType::Colon)?;
4531            match field.as_str() {
4532                "mode" => node.mode = self.consume_any_ident_or_kw()?.value,
4533                "novelty" => node.novelty = self.consume_number()?,
4534                "depth" => {
4535                    node.depth = self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0)
4536                }
4537                "branches" => {
4538                    node.branches =
4539                        self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0)
4540                }
4541                "constraints" => node.constraints_ref = self.consume_any_ident_or_kw()?.value,
4542                other => {
4543                    return Err(self.error(&format!("unknown forge field `{other}`")))
4544                }
4545            }
4546            if self.check(TokenType::Comma) {
4547                self.consume(TokenType::Comma)?;
4548            }
4549        }
4550        self.consume(TokenType::RBrace)?;
4551        Ok(node)
4552    }
4553
4554    /// §Fase 65 — Parse `par { stmt1  stmt2  … }` into CONCURRENT branches.
4555    /// Each top-level flow statement inside the block is one branch (a
4556    /// single-statement body); they execute concurrently at runtime
4557    /// (`flow_dispatcher::parallel::run_branches_concurrently`). Before §65 the
4558    /// `par` body was skipped (`parse_block_step`), so the branches were lost
4559    /// and the handler ran as a stub. Multi-statement branches (grouping
4560    /// several steps into one sequential branch) are a future grammar
4561    /// extension; today the natural `par { step A  step B }` fans A and B out.
4562    fn parse_par_block(&mut self) -> Result<ParBlock, ParseError> {
4563        let tok = self.current().clone();
4564        self.advance(); // consume `par`
4565        self.consume(TokenType::LBrace)?;
4566        let mut branches: Vec<Vec<FlowStep>> = Vec::new();
4567        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4568            branches.push(vec![self.parse_flow_step()?]);
4569        }
4570        self.consume(TokenType::RBrace)?;
4571        Ok(ParBlock {
4572            branches,
4573            loc: Loc {
4574                line: tok.line,
4575                column: tok.column,
4576            },
4577        })
4578    }
4579
4580    /// §Fase 51.a — Parse the `quant` cognitive block surface.
4581    ///
4582    /// Grammar (the attribute header is OPTIONAL):
4583    /// ```text
4584    /// quant { <flow steps> }
4585    /// quant(encoding: amplitude, observable: M, qubits: 10,
4586    ///       depth: 4, bandwidth: 0.5, reupload: 3, backend: quant_sim) { <flow steps> }
4587    /// ```
4588    /// The bare form (the paper's example) leaves every attribute defaulted
4589    /// (`encoding = amplitude`, `effect = quant_sim`). The body is parsed into
4590    /// real nested `FlowStep`s — like `par` branches — so §51.b's Continuous
4591    /// Type Invariant scans actual AST rather than skipped tokens.
4592    /// §Fase 88.a — parse `warden(<target>) within <Scope> { <body> }`. The
4593    /// `within <Scope>` clause is MANDATORY at the grammar level (fail-closed by
4594    /// construction: a scopeless warden cannot be written); §88.c checks the
4595    /// scope RESOLVES + the target is in its allowlist.
4596    fn parse_warden(&mut self) -> Result<WardenBlock, ParseError> {
4597        let tok = self.consume(TokenType::Warden)?;
4598        // `(<target>)` — the resource under analysis.
4599        self.consume(TokenType::LParen)?;
4600        let target = self.consume_any_ident_or_kw()?.value;
4601        self.consume(TokenType::RParen)?;
4602        // `within <Scope>` — MANDATORY. Omitting it is a hard parse error.
4603        self.consume(TokenType::Within)?;
4604        let scope_ref = self.consume(TokenType::Identifier)?.value;
4605        let mut block = WardenBlock {
4606            target,
4607            scope_ref,
4608            body: Vec::new(),
4609            loc: Loc {
4610                line: tok.line,
4611                column: tok.column,
4612            },
4613        };
4614        // Body: real nested flow steps (like `quant`/`par`).
4615        self.consume(TokenType::LBrace)?;
4616        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4617            block.body.push(self.parse_flow_step()?);
4618        }
4619        self.consume(TokenType::RBrace)?;
4620        Ok(block)
4621    }
4622
4623    /// §Fase 88.a — parse `scope <Name> { targets: [ … ], depth: <ident>,
4624    /// approver: [requires] "<cap>" }`. Flat key:value block (the `cache` shape).
4625    /// Catalog + non-empty validation is §88.c. Unknown fields are a hard error
4626    /// (D83.7): a scope governs an offensive-capable analysis.
4627    fn parse_scope(&mut self) -> Result<ScopeDefinition, ParseError> {
4628        let tok = self.consume(TokenType::Scope)?;
4629        let name = self.consume(TokenType::Identifier)?.value;
4630        let mut node = ScopeDefinition {
4631            name,
4632            loc: Loc {
4633                line: tok.line,
4634                column: tok.column,
4635            },
4636            ..Default::default()
4637        };
4638        self.consume(TokenType::LBrace)?;
4639        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4640            let key = self.consume_any_ident_or_kw()?.value;
4641            self.consume(TokenType::Colon)?;
4642            match key.as_str() {
4643                "targets" => {
4644                    self.consume(TokenType::LBracket)?;
4645                    while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
4646                        let t = if self.check(TokenType::StringLit) {
4647                            self.consume(TokenType::StringLit)?.value
4648                        } else {
4649                            self.consume_any_ident_or_kw()?.value
4650                        };
4651                        node.targets.push(t);
4652                        if self.check(TokenType::Comma) {
4653                            self.advance();
4654                        }
4655                    }
4656                    self.consume(TokenType::RBracket)?;
4657                }
4658                "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
4659                "approver" => {
4660                    // Optional `requires` sugar before the capability string.
4661                    if self.current().value == "requires" {
4662                        self.advance();
4663                    }
4664                    node.approver = self.consume(TokenType::StringLit)?.value;
4665                }
4666                other => {
4667                    return Err(self.error(&format!(
4668                        "unknown scope field `{other}` in scope `{}` — expected \
4669                         `targets` / `depth` / `approver`",
4670                        node.name
4671                    )))
4672                }
4673            }
4674            if self.check(TokenType::Comma) {
4675                self.consume(TokenType::Comma)?;
4676            }
4677        }
4678        self.consume(TokenType::RBrace)?;
4679        Ok(node)
4680    }
4681
4682    fn parse_quant(&mut self) -> Result<QuantBlock, ParseError> {
4683        let tok = self.current().clone();
4684        self.advance(); // consume `quant`
4685
4686        let mut block = QuantBlock {
4687            encoding: None,
4688            observable: None,
4689            qubits: None,
4690            depth: None,
4691            bandwidth: None,
4692            reupload: None,
4693            // D1/D9 default backend: the CPU simulator effect. `qpu_native` is
4694            // opt-in via `backend: qpu_native`.
4695            effect: "quant_sim".to_string(),
4696            body: Vec::new(),
4697            loc: Loc {
4698                line: tok.line,
4699                column: tok.column,
4700            },
4701        };
4702
4703        // ── Optional attribute header: `(key: value, …)` ──
4704        if self.check(TokenType::LParen) {
4705            self.advance();
4706            while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
4707                let key = self.consume_any_ident_or_kw()?.value;
4708                self.consume(TokenType::Colon)?;
4709                match key.as_str() {
4710                    "encoding" => {
4711                        block.encoding = Some(self.consume_any_ident_or_kw()?.value)
4712                    }
4713                    "observable" => {
4714                        block.observable = Some(self.parse_dotted_identifier()?)
4715                    }
4716                    "qubits" => block.qubits = Some(self.consume_number()? as i64),
4717                    "depth" => block.depth = Some(self.consume_number()? as i64),
4718                    "bandwidth" => block.bandwidth = Some(self.consume_number()?),
4719                    // §Fase 69.c — data re-uploading layers.
4720                    "reupload" => block.reupload = Some(self.consume_number()? as i64),
4721                    // `backend:` selects the algebraic-effect tag (D1/D9).
4722                    "backend" => block.effect = self.consume_any_ident_or_kw()?.value,
4723                    other => {
4724                        return Err(ParseError {
4725                            message: format!(
4726                                "Unknown `quant` attribute `{other}` — expected one of \
4727                                 encoding, observable, qubits, depth, bandwidth, reupload, backend"
4728                            ),
4729                            line: self.current().line,
4730                            column: self.current().column,
4731                            ..Default::default()
4732                        });
4733                    }
4734                }
4735                // Optional comma between attributes (order-free, trailing-comma ok).
4736                if self.check(TokenType::Comma) {
4737                    self.advance();
4738                }
4739            }
4740            self.consume(TokenType::RParen)?;
4741        }
4742
4743        // ── Body: real nested flow steps (like `par`) ──
4744        self.consume(TokenType::LBrace)?;
4745        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4746            block.body.push(self.parse_flow_step()?);
4747        }
4748        self.consume(TokenType::RBrace)?;
4749
4750        Ok(block)
4751    }
4752
4753    /// §Fase 51.d.2 — Parse the `yield <expr>` measurement point. Reuses the
4754    /// `let`-value expression grammar (reference / literal / arithmetic) so the
4755    /// yielded value's tokenization intent is preserved in `value_kind`.
4756    fn parse_yield(&mut self) -> Result<YieldStatement, ParseError> {
4757        let tok = self.consume(TokenType::Yield)?;
4758        let loc = self.loc_of(&tok);
4759        self.last_let_value_kind = "literal".to_string();
4760        let value_expr = self.parse_let_value_expr()?;
4761        Ok(YieldStatement {
4762            value_expr,
4763            value_kind: self.last_let_value_kind.clone(),
4764            loc,
4765        })
4766    }
4767
4768    /// Parse: keyword Name on target -> output_type (apply pattern).
4769    /// §Fase 111.f — `compute <Name> on <a>, <b>, … -> <out>`.
4770    ///
4771    /// Positional arguments, bound to the compute's declared parameters in order.
4772    /// The generic [`Self::parse_apply_step`] captured a single `on <target>` and
4773    /// then the call site threw even that away (`arguments: Vec::new()`).
4774    fn parse_compute_apply(&mut self) -> Result<ComputeApplyStep, ParseError> {
4775        let tok = self.current().clone();
4776        let loc = self.loc_of(&tok);
4777        self.advance(); // consume `compute`
4778        let compute_name = self.consume_any_ident_or_kw()?.value.clone();
4779
4780        let mut arguments = Vec::new();
4781        if self.current().value == "on" {
4782            self.advance();
4783            loop {
4784                arguments.push(self.consume_any_ident_or_kw()?.value.clone());
4785                if self.check(TokenType::Comma) {
4786                    self.advance();
4787                } else {
4788                    break;
4789                }
4790            }
4791        }
4792
4793        let mut output_name = String::new();
4794        if self.check(TokenType::Arrow) {
4795            self.advance();
4796            output_name = self.consume_any_ident_or_kw()?.value.clone();
4797        }
4798
4799        Ok(ComputeApplyStep {
4800            compute_name,
4801            arguments,
4802            output_name,
4803            loc,
4804        })
4805    }
4806
4807    fn parse_apply_step(&mut self, _kw: &str) -> Result<(Loc, String, String, String), ParseError> {
4808        let tok = self.current().clone();
4809        self.advance(); // consume keyword
4810        let name = self.consume_any_ident_or_kw()?.value.clone();
4811        let mut target = String::new();
4812        let mut output_type = String::new();
4813        // "on" target
4814        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4815            let next = self.current().clone();
4816            if next.value == "on" {
4817                self.advance();
4818                target = self.consume_any_ident_or_kw()?.value.clone();
4819            }
4820        }
4821        // -> output_type
4822        if self.check(TokenType::Arrow) {
4823            self.advance();
4824            output_type = self.consume_any_ident_or_kw()?.value.clone();
4825        }
4826        // Skip optional braced block
4827        if self.check(TokenType::LBrace) {
4828            self.skip_braced_block()?;
4829        }
4830        Ok((
4831            Loc {
4832                line: tok.line,
4833                column: tok.column,
4834            },
4835            name,
4836            target,
4837            output_type,
4838        ))
4839    }
4840
4841    /// §Fase 119 (D119.4) — `<kind> <Name> [on <target>] [-> <binding>]` inside
4842    /// a `step { }` body.
4843    ///
4844    /// Differences from the flow-level `parse_apply_step`, both deliberate:
4845    ///
4846    /// - The target may be a CALL EXPRESSION, captured verbatim: README block
4847    ///   42 writes `mandate LegalPrecision on ContractDrafter(terms)`. The
4848    ///   flow-level form never needed this; the published step-level form does.
4849    /// - No trailing braced block is skipped. A guard is one statement; a
4850    ///   silently-skipped block after it would be the §119.b.1 defect again.
4851    fn parse_step_guard(&mut self, kind: &str) -> Result<StepGuardNode, ParseError> {
4852        let tok = self.current().clone();
4853        self.advance(); // consume the keyword
4854        let name = self.consume_any_ident_or_kw()?.value.clone();
4855        let mut target = String::new();
4856        let mut binding = String::new();
4857        if self.current().value == "on" {
4858            self.advance();
4859            target = self.consume_any_ident_or_kw()?.value.clone();
4860            // `ContractDrafter(terms)` — capture the balanced argument list
4861            // verbatim into the target string.
4862            if self.check(TokenType::LParen) {
4863                let mut depth = 0usize;
4864                loop {
4865                    let t = self.current().clone();
4866                    match t.ttype {
4867                        TokenType::LParen => depth += 1,
4868                        TokenType::RParen => depth -= 1,
4869                        TokenType::Eof => {
4870                            return Err(ParseError {
4871                                message: format!(
4872                                    "unterminated argument list in `{kind} {name} on {target}(…`"
4873                                ),
4874                                line: t.line,
4875                                column: t.column,
4876                                ..Default::default()
4877                            })
4878                        }
4879                        _ => {}
4880                    }
4881                    target.push_str(&t.value);
4882                    self.advance();
4883                    if depth == 0 {
4884                        break;
4885                    }
4886                }
4887            }
4888        }
4889        if self.check(TokenType::Arrow) {
4890            self.advance();
4891            binding = self.consume_any_ident_or_kw()?.value.clone();
4892        }
4893        Ok(StepGuardNode {
4894            kind: kind.to_string(),
4895            name,
4896            target,
4897            binding,
4898            loc: Loc {
4899                line: tok.line,
4900                column: tok.column,
4901            },
4902        })
4903    }
4904
4905    fn parse_weave_step(&mut self) -> Result<FlowStep, ParseError> {
4906        let tok = self.current().clone();
4907        self.advance();
4908        let mut node = WeaveStep {
4909            sources: Vec::new(),
4910            target: String::new(),
4911            format_type: String::new(),
4912            priority: Vec::new(),
4913            style: String::new(),
4914            loc: Loc {
4915                line: tok.line,
4916                column: tok.column,
4917            },
4918        };
4919        if self.check(TokenType::LBrace) {
4920            self.advance();
4921            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4922                let f = self.current().value.clone();
4923                self.advance();
4924                if self.check(TokenType::Colon) {
4925                    self.advance();
4926                    match f.as_str() {
4927                        "sources" => node.sources = self.parse_bracketed_identifiers()?,
4928                        "target" => node.target = self.consume_any_ident_or_kw()?.value.clone(),
4929                        "format" => {
4930                            node.format_type = self.consume_any_ident_or_kw()?.value.clone()
4931                        }
4932                        "priority" => node.priority = self.parse_bracketed_identifiers()?,
4933                        "style" => node.style = self.consume_any_ident_or_kw()?.value.clone(),
4934                        _ => self.skip_value(),
4935                    }
4936                }
4937            }
4938            if self.check(TokenType::RBrace) {
4939                self.advance();
4940            }
4941        }
4942        Ok(FlowStep::Weave(node))
4943    }
4944
4945    fn parse_use_step(&mut self) -> Result<FlowStep, ParseError> {
4946        let tok = self.current().clone();
4947        self.advance();
4948        let tool_name = self.consume_any_ident_or_kw()?.value.clone();
4949        // §Fase 58.b — two mutually-exclusive `use` argument surfaces:
4950        //   * `use Tool(query = "${q}", max_results = 5)` — D2 canonical
4951        //     multi-field keyword args (§58.b `UseArgs::Named`).
4952        //   * `use Tool on "${arg}"` / `on query` — the §54.b single positional
4953        //     argument (D5 back-compat, `UseArgs::LegacyPositional`):
4954        //       - a STRING LITERAL carrying interpolation (`on "${query}"`)
4955        //         resolved at dispatch against request-bound flow params;
4956        //       - a BARE identifier / literal (`on query` / `on 42`) verbatim.
4957        //     (Unquoted `${query}` is intentionally NOT a form — interpolation
4958        //     lives inside string literals everywhere in Axon.)
4959        let args = if self.check(TokenType::LParen) {
4960            UseArgs::Named(self.parse_named_arg_list()?)
4961        } else {
4962            let mut argument = String::new();
4963            if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4964                let next = self.current().clone();
4965                if next.value == "on" {
4966                    self.advance();
4967                    argument = self.consume_any_ident_or_kw()?.value.clone();
4968                }
4969            }
4970            UseArgs::LegacyPositional(argument)
4971        };
4972        if self.check(TokenType::LBrace) {
4973            self.skip_braced_block()?;
4974        }
4975        Ok(FlowStep::UseTool(UseToolStep {
4976            tool_name,
4977            args,
4978            loc: Loc {
4979                line: tok.line,
4980                column: tok.column,
4981            },
4982        }))
4983    }
4984
4985    /// §Fase 58.b — parse `(name = value, …)` keyword args for the canonical
4986    /// `use Tool(...)` multi-field dispatch. Values are captured as expression
4987    /// strings (StringLit / Integer / Float / Bool / dotted identifier / list)
4988    /// via the shared `parse_let_atom`, since the frontend has no structured
4989    /// `Expr`. A trailing comma is tolerated; `()` yields no args.
4990    fn parse_named_arg_list(&mut self) -> Result<Vec<(String, String, String)>, ParseError> {
4991        self.consume(TokenType::LParen)?;
4992        let mut args = Vec::new();
4993        while !self.check(TokenType::RParen) {
4994            // Accept a keyword-as-name (`filter`, `type`, `from`, …) — real
4995            // adopter schemas use such names; the following `=` disambiguates.
4996            let name = self.consume_any_ident_or_kw()?.value;
4997            self.consume(TokenType::Assign)?;
4998            let value = self.parse_let_atom()?;
4999            // §Fase 60 — `parse_let_atom` classified the value (`"literal"` vs
5000            // `"reference"`); carry it so the runtime resolves a bare
5001            // identifier / `Step.output` as a binding lookup, not a literal.
5002            let value_kind = self.last_let_value_kind.clone();
5003            args.push((name, value, value_kind));
5004            if self.check(TokenType::Comma) {
5005                self.advance();
5006            } else {
5007                break;
5008            }
5009        }
5010        self.consume(TokenType::RParen)?;
5011        Ok(args)
5012    }
5013
5014    fn parse_remember_step(&mut self) -> Result<FlowStep, ParseError> {
5015        let tok = self.current().clone();
5016        self.advance();
5017        let expr = self.consume_any_ident_or_kw()?.value.clone();
5018        let mut mem = String::new();
5019        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
5020            let next = self.current().clone();
5021            if next.value == "in" || next.ttype == TokenType::In {
5022                self.advance();
5023                mem = self.consume_any_ident_or_kw()?.value.clone();
5024            }
5025        }
5026        Ok(FlowStep::Remember(RememberStep {
5027            expression: expr,
5028            memory_target: mem,
5029            loc: Loc {
5030                line: tok.line,
5031                column: tok.column,
5032            },
5033        }))
5034    }
5035
5036    fn parse_recall_step(&mut self) -> Result<FlowStep, ParseError> {
5037        let tok = self.current().clone();
5038        self.advance();
5039        let query = if self.check(TokenType::StringLit) {
5040            self.consume(TokenType::StringLit)?.value.clone()
5041        } else {
5042            self.consume_any_ident_or_kw()?.value.clone()
5043        };
5044        let mut mem = String::new();
5045        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
5046            let next = self.current().clone();
5047            if next.value == "from" || next.ttype == TokenType::From {
5048                self.advance();
5049                mem = self.consume_any_ident_or_kw()?.value.clone();
5050            }
5051        }
5052        Ok(FlowStep::Recall(RecallStep {
5053            query,
5054            memory_source: mem,
5055            loc: Loc {
5056                line: tok.line,
5057                column: tok.column,
5058            },
5059        }))
5060    }
5061
5062    fn parse_hibernate_step(&mut self) -> Result<FlowStep, ParseError> {
5063        let tok = self.current().clone();
5064        self.advance();
5065        let mut event = String::new();
5066        let mut timeout = String::new();
5067        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
5068            // §Fase 119.d — README §III writes `hibernate until "event_name"`
5069            // (the `until` keyword + a STRING event). The parser accepted only
5070            // the bare-identifier form, so the published block never compiled.
5071            // Both forms resolve to the same field.
5072            let first = self.consume_any_ident_or_kw()?.value.clone();
5073            if first == "until" && self.check(TokenType::StringLit) {
5074                event = self.consume(TokenType::StringLit)?.value.clone();
5075            } else {
5076                event = first;
5077            }
5078        }
5079        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
5080            let next = self.current().clone();
5081            if next.ttype == TokenType::Duration {
5082                self.advance();
5083                timeout = next.value.clone();
5084            }
5085        }
5086        Ok(FlowStep::Hibernate(HibernateStep {
5087            event_name: event,
5088            timeout,
5089            loc: Loc {
5090                line: tok.line,
5091                column: tok.column,
5092            },
5093        }))
5094    }
5095
5096    /// §Fase 108.d — `focus <Dataspace> { where: "<filter>", select: [cols], as: <name> }`
5097    /// — σ_φ ∘ π_v over a declared dataspace. The `where:` string is the
5098    /// §35 data-plane filter grammar (D108.9, shared with retrieve /
5099    /// navigate). Pre-108.d the optional body was silently discarded.
5100    /// §Fase 109.a — `grad <letName> wrt <x> [as <name>]` /
5101    /// `grad <letName> wrt [a, b] as <name>`. The differentiation itself
5102    /// happens at CHECK/IR time (T931/T932 + the symbolic differentiator);
5103    /// the parser only captures the surface.
5104    fn parse_grad_step(&mut self) -> Result<FlowStep, ParseError> {
5105        let tok = self.current().clone();
5106        self.advance();
5107        let target = self.consume_any_ident_or_kw()?.value.clone();
5108        let mut wrt: Vec<String> = Vec::new();
5109        let mut output = String::new();
5110        if !self.at_declaration_start() && self.current().value == "wrt" {
5111            self.advance();
5112            if self.check(TokenType::LBracket) {
5113                wrt = self.parse_bracketed_identifiers()?;
5114            } else {
5115                wrt.push(self.consume_any_ident_or_kw()?.value.clone());
5116            }
5117        }
5118        if !self.at_declaration_start() && self.current().value == "as" {
5119            self.advance();
5120            output = self.consume_any_ident_or_kw()?.value.clone();
5121        }
5122        Ok(FlowStep::Grad(GradStep {
5123            target,
5124            wrt,
5125            output,
5126            loc: Loc {
5127                line: tok.line,
5128                column: tok.column,
5129            },
5130        }))
5131    }
5132
5133    fn parse_focus_step(&mut self) -> Result<FlowStep, ParseError> {
5134        let tok = self.current().clone();
5135        self.advance();
5136        let expression = if self.at_declaration_start()
5137            || self.check(TokenType::RBrace)
5138            || self.check(TokenType::Eof)
5139        {
5140            String::new()
5141        } else {
5142            self.consume_any_ident_or_kw()?.value.clone()
5143        };
5144        let mut where_expr = String::new();
5145        let mut select: Vec<String> = Vec::new();
5146        let mut output = String::new();
5147        if self.check(TokenType::LBrace) {
5148            self.advance();
5149            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5150                if self.check(TokenType::Comma) {
5151                    self.advance();
5152                    continue;
5153                }
5154                let f = self.current().value.clone();
5155                self.advance();
5156                if self.check(TokenType::Colon) {
5157                    self.advance();
5158                    match f.as_str() {
5159                        "where" => {
5160                            where_expr = self.consume(TokenType::StringLit)?.value.clone()
5161                        }
5162                        "select" => select = self.parse_bracketed_identifiers()?,
5163                        "as" | "alias" => {
5164                            output = self.consume_any_ident_or_kw()?.value.clone()
5165                        }
5166                        _ => self.skip_value(),
5167                    }
5168                }
5169            }
5170            if self.check(TokenType::RBrace) {
5171                self.advance();
5172            }
5173        }
5174        Ok(FlowStep::Focus(FocusStep {
5175            expression,
5176            where_expr,
5177            select,
5178            output,
5179            loc: Loc {
5180                line: tok.line,
5181                column: tok.column,
5182            },
5183        }))
5184    }
5185
5186    fn parse_associate_step(&mut self) -> Result<FlowStep, ParseError> {
5187        let tok = self.current().clone();
5188        self.advance();
5189        let left = self.consume_any_ident_or_kw()?.value.clone();
5190        let mut right = String::new();
5191        let mut using = String::new();
5192        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
5193            right = self.consume_any_ident_or_kw()?.value.clone();
5194        }
5195        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
5196            let next = self.current().clone();
5197            if next.value == "using" {
5198                self.advance();
5199                using = self.consume_any_ident_or_kw()?.value.clone();
5200            }
5201        }
5202        let mut output = String::new();
5203        if self.check(TokenType::LBrace) {
5204            self.advance();
5205            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5206                let f = self.current().value.clone();
5207                self.advance();
5208                if self.check(TokenType::Colon) {
5209                    self.advance();
5210                    match f.as_str() {
5211                        "as" | "alias" => output = self.consume_any_ident_or_kw()?.value.clone(),
5212                        _ => self.skip_value(),
5213                    }
5214                }
5215            }
5216            if self.check(TokenType::RBrace) {
5217                self.advance();
5218            }
5219        }
5220        Ok(FlowStep::Associate(AssociateStep {
5221            left,
5222            right,
5223            using_field: using,
5224            output,
5225            loc: Loc {
5226                line: tok.line,
5227                column: tok.column,
5228            },
5229        }))
5230    }
5231
5232    fn parse_aggregate_step(&mut self) -> Result<FlowStep, ParseError> {
5233        let tok = self.current().clone();
5234        self.advance();
5235        let target = self.consume_any_ident_or_kw()?.value.clone();
5236        let mut group_by = Vec::new();
5237        let mut alias = String::new();
5238        let mut compute: Vec<String> = Vec::new();
5239        let mut where_expr = String::new();
5240        if self.check(TokenType::LBrace) {
5241            self.advance();
5242            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5243                let f = self.current().value.clone();
5244                self.advance();
5245                if self.check(TokenType::Colon) {
5246                    self.advance();
5247                    match f.as_str() {
5248                        "group_by" => group_by = self.parse_bracketed_identifiers()?,
5249                        "alias" | "as" => alias = self.consume_any_ident_or_kw()?.value.clone(),
5250                        // §Fase 108.d — the closed aggregate catalog, kept
5251                        // RAW (`count`, `sum(score)`, …); T930 validates.
5252                        "compute" => compute = self.parse_bracketed_aggregates()?,
5253                        // §Fase 108.d — the data-plane where (D108.9).
5254                        "where" => where_expr = self.consume(TokenType::StringLit)?.value.clone(),
5255                        _ => self.skip_value(),
5256                    }
5257                }
5258            }
5259            if self.check(TokenType::RBrace) {
5260                self.advance();
5261            }
5262        }
5263        Ok(FlowStep::Aggregate(AggregateStep {
5264            target,
5265            group_by,
5266            alias,
5267            compute,
5268            where_expr,
5269            loc: Loc {
5270                line: tok.line,
5271                column: tok.column,
5272            },
5273        }))
5274    }
5275
5276    fn parse_explore_step(&mut self) -> Result<FlowStep, ParseError> {
5277        let tok = self.current().clone();
5278        self.advance();
5279        let target = self.consume_any_ident_or_kw()?.value.clone();
5280        let mut limit = None;
5281        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
5282            if self.current().ttype == TokenType::Integer {
5283                limit = self.current().value.parse::<i64>().ok();
5284                self.advance();
5285            }
5286        }
5287        let mut output = String::new();
5288        if self.check(TokenType::LBrace) {
5289            self.advance();
5290            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5291                let f = self.current().value.clone();
5292                self.advance();
5293                if self.check(TokenType::Colon) {
5294                    self.advance();
5295                    match f.as_str() {
5296                        "as" | "alias" => output = self.consume_any_ident_or_kw()?.value.clone(),
5297                        _ => self.skip_value(),
5298                    }
5299                }
5300            }
5301            if self.check(TokenType::RBrace) {
5302                self.advance();
5303            }
5304        }
5305        Ok(FlowStep::ExploreStep(ExploreStepNode {
5306            target,
5307            limit,
5308            output,
5309            loc: Loc {
5310                line: tok.line,
5311                column: tok.column,
5312            },
5313        }))
5314    }
5315
5316    /// §Fase 108.d — parse `[count, sum(score), avg(x)]`: bracketed
5317    /// aggregate entries, each `ident` or `ident(ident)`, kept raw.
5318    fn parse_bracketed_aggregates(&mut self) -> Result<Vec<String>, ParseError> {
5319        let mut out = Vec::new();
5320        self.consume(TokenType::LBracket)?;
5321        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
5322            let name = self.consume_any_ident_or_kw()?.value.clone();
5323            if self.check(TokenType::LParen) {
5324                self.advance();
5325                let col = self.consume_any_ident_or_kw()?.value.clone();
5326                self.consume(TokenType::RParen)?;
5327                out.push(format!("{name}({col})"));
5328            } else {
5329                out.push(name);
5330            }
5331            if self.check(TokenType::Comma) {
5332                self.advance();
5333            }
5334        }
5335        self.consume(TokenType::RBracket)?;
5336        Ok(out)
5337    }
5338
5339    /// §Fase 108.c — the governed ingest step:
5340    ///
5341    /// ```text
5342    /// ingest <sourceRef> into <Dataspace> {
5343    ///     format: csv | json
5344    ///     limits { max_bytes: N, max_rows: N }
5345    /// }
5346    /// ```
5347    ///
5348    /// Until 108.c the body was consumed by `skip_braced_block()`. Now it
5349    /// is a closed grammar: `format:` (raw here; required + validated by
5350    /// `axon-T929`) and an optional `limits { … }` block whose bounds are
5351    /// enforced on the raw byte stream BEFORE parsing (§100). An unknown
5352    /// body entry is a parse error.
5353    fn parse_ingest_step(&mut self) -> Result<FlowStep, ParseError> {
5354        let tok = self.current().clone();
5355        self.advance();
5356        let source = self.consume_any_ident_or_kw()?.value.clone();
5357        let mut target = String::new();
5358        let mut format = String::new();
5359        let mut max_bytes: Option<u64> = None;
5360        let mut max_rows: Option<u64> = None;
5361        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
5362            let next = self.current().clone();
5363            if next.value == "into" || next.ttype == TokenType::Into {
5364                self.advance();
5365                target = self.consume_any_ident_or_kw()?.value.clone();
5366            }
5367        }
5368        if self.check(TokenType::LBrace) {
5369            self.consume(TokenType::LBrace)?;
5370            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5371                // Optional separators between body entries.
5372                if self.check(TokenType::Comma) {
5373                    self.advance();
5374                    continue;
5375                }
5376                let entry = self.current().clone();
5377                match entry.value.as_str() {
5378                    "format" => {
5379                        self.advance();
5380                        self.consume(TokenType::Colon)?;
5381                        format = self.consume_any_ident_or_kw()?.value.clone();
5382                    }
5383                    "limits" => {
5384                        self.advance();
5385                        self.consume(TokenType::LBrace)?;
5386                        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5387                            let bound = self.current().clone();
5388                            self.advance();
5389                            self.consume(TokenType::Colon)?;
5390                            let num_tok = self.consume(TokenType::Integer)?.clone();
5391                            let value = num_tok.value.parse::<u64>().map_err(|_| ParseError {
5392                                message: format!(
5393                                    "ingest `limits` bound `{}` must be a non-negative \
5394                                     integer byte/row count, got `{}`.",
5395                                    bound.value, num_tok.value
5396                                ),
5397                                line: num_tok.line,
5398                                column: num_tok.column,
5399                                ..Default::default()
5400                            })?;
5401                            match bound.value.as_str() {
5402                                "max_bytes" => max_bytes = Some(value),
5403                                "max_rows" => max_rows = Some(value),
5404                                other => {
5405                                    return Err(ParseError {
5406                                        message: format!(
5407                                            "Unknown ingest limit `{other}`. The closed \
5408                                             limits grammar is `max_bytes: <N>` and \
5409                                             `max_rows: <N>` — bounds enforced on the raw \
5410                                             stream BEFORE parsing (§100).",
5411                                        ),
5412                                        line: bound.line,
5413                                        column: bound.column,
5414                                        ..Default::default()
5415                                    });
5416                                }
5417                            }
5418                            if self.check(TokenType::Comma) {
5419                                self.advance();
5420                            }
5421                        }
5422                        self.consume(TokenType::RBrace)?;
5423                    }
5424                    other => {
5425                        return Err(ParseError {
5426                            message: format!(
5427                                "Unknown entry `{other}` in ingest body. The closed \
5428                                 grammar is `format: csv|json` and \
5429                                 `limits {{ max_bytes: <N>, max_rows: <N> }}`.",
5430                            ),
5431                            line: entry.line,
5432                            column: entry.column,
5433                            ..Default::default()
5434                        });
5435                    }
5436                }
5437            }
5438            self.consume(TokenType::RBrace)?;
5439        }
5440        Ok(FlowStep::Ingest(IngestStep {
5441            source,
5442            target,
5443            format,
5444            max_bytes,
5445            max_rows,
5446            loc: Loc {
5447                line: tok.line,
5448                column: tok.column,
5449            },
5450        }))
5451    }
5452
5453    /// §Fase 119.f — is the cursor on a `navigate` field (`<name>:`)?
5454    ///
5455    /// The continuation test for the braceless field list. Closed catalog by
5456    /// construction: a name outside it ends the navigate and belongs to the
5457    /// enclosing step, which is exactly what makes the delimiter-free form
5458    /// unambiguous.
5459    fn at_navigate_field(&self) -> bool {
5460        const FIELDS: &[&str] = &[
5461            // §Fase 119.f — `output` is deliberately ABSENT from the
5462            // BRACELESS catalog even though the braced form accepts it as an
5463            // alias for `as`. In step-body position `output:` is the STEP's
5464            // own field, and a shared name would make the terminator
5465            // ambiguous — the braceless navigate would swallow the step's
5466            // output type. README writes `as:` in this position throughout;
5467            // the braced/flow-level form keeps both spellings.
5468            "corpus", "query", "trail", "as", "from", "budget", "where",
5469            "depth", "recall",
5470        ];
5471        self.field_ahead(FIELDS)
5472    }
5473
5474    /// §Fase 119.f — the same test for `drill`.
5475    fn at_drill_field(&self) -> bool {
5476        // Same reason as `at_navigate_field`: no `output` in the braceless
5477        // catalog, because that name belongs to the enclosing step.
5478        const FIELDS: &[&str] = &["subtree", "path", "query", "as"];
5479        self.field_ahead(FIELDS)
5480    }
5481
5482    /// `<one of names>` immediately followed by `:`.
5483    fn field_ahead(&self, names: &[&str]) -> bool {
5484        let cur = self.current();
5485        if !names.contains(&cur.value.as_str()) {
5486            return false;
5487        }
5488        self.tokens
5489            .get(self.pos + 1)
5490            .is_some_and(|t| t.ttype == TokenType::Colon)
5491    }
5492
5493    /// §Fase 119.f — a CONFIG KEY: `"env:DATABASE_URL"` or the bare
5494    /// `env:DATABASE_URL` README publishes.
5495    ///
5496    /// §113 made `connection:`/`endpoint:` a config KEY rather than a URL or a
5497    /// DSN — the address resolves per deployment. README writes both the
5498    /// quoted and the bare spelling; the parser took only the quoted one, so
5499    /// every published `axonstore` with an unquoted key failed on its own
5500    /// third line. One value, two spellings — the epsilon/tolerance
5501    /// resolution of §119.b.1, applied to the config surface.
5502    fn parse_config_key(&mut self) -> Result<String, ParseError> {
5503        if self.check(TokenType::StringLit) {
5504            return Ok(self.consume(TokenType::StringLit)?.value.clone());
5505        }
5506        let scheme = self.consume_any_ident_or_kw()?.value.clone();
5507        if self.check(TokenType::Colon) {
5508            self.advance();
5509            let key = self.consume_any_ident_or_kw()?.value.clone();
5510            return Ok(format!("{scheme}:{key}"));
5511        }
5512        Ok(scheme)
5513    }
5514
5515    /// §Fase 119.f — a PIX field value: a string literal OR a binding
5516    /// reference. README writes `query: question` (the flow parameter) far
5517    /// more often than a literal, and the parser accepted only the literal —
5518    /// which is why every published `navigate` failed on its own second line.
5519    fn parse_pix_value(&mut self) -> Result<String, ParseError> {
5520        if self.check(TokenType::StringLit) {
5521            return Ok(self.consume(TokenType::StringLit)?.value.clone());
5522        }
5523        Ok(self.consume_any_ident_or_kw()?.value.clone())
5524    }
5525
5526    fn parse_navigate_step(&mut self) -> Result<FlowStep, ParseError> {
5527        let tok = self.current().clone();
5528        self.advance();
5529        let pix_name = self.consume_any_ident_or_kw()?.value.clone();
5530        let mut node = NavigateStep {
5531            depth: None,
5532            pix_name,
5533            corpus_name: String::new(),
5534            query_expr: String::new(),
5535            trail_enabled: false,
5536            output_name: String::new(),
5537            seed: String::new(),
5538            budget: None,
5539            where_expr: String::new(),
5540            loc: Loc {
5541                line: tok.line,
5542                column: tok.column,
5543            },
5544        };
5545        // §Fase 119.f — the BRACELESS field form, which is what README §pix/
5546        // §corpus publishes everywhere:
5547        //
5548        //     navigate ContractIndex
5549        //         query: question
5550        //         trail: enabled
5551        //         as: relevant_sections
5552        //
5553        // Terminated by the field-name set, not by a brace: the navigate
5554        // fields are a CLOSED catalog, so "the next token is one of these and
5555        // is followed by a colon" is an unambiguous continuation test. That is
5556        // the same closed-catalog discipline the rest of the language uses,
5557        // and it is why this form needs no delimiter to be parseable.
5558        if !self.check(TokenType::LBrace) {
5559            while self.at_navigate_field() {
5560                let f = self.current().value.clone();
5561                self.advance();
5562                self.consume(TokenType::Colon)?;
5563                match f.as_str() {
5564                    "corpus" => node.corpus_name = self.consume_any_ident_or_kw()?.value.clone(),
5565                    "query" => node.query_expr = self.parse_pix_value()?,
5566                    "trail" => {
5567                        let v = self.consume_any_ident_or_kw()?.value;
5568                        node.trail_enabled = matches!(v.as_str(), "true" | "enabled" | "on");
5569                    }
5570                    "output" | "as" => {
5571                        node.output_name = self.consume_any_ident_or_kw()?.value.clone()
5572                    }
5573                    "from" => node.seed = self.consume_any_ident_or_kw()?.value.clone(),
5574                    "budget" => node.budget = self.parse_optional_int(),
5575                    "where" => node.where_expr = self.parse_pix_value()?,
5576                    "depth" => node.depth = self.parse_optional_int(),
5577                    // §Fase 119.f — `recall: episodic` selects the MDN memory
5578                    // mode README's clinical/legal examples write. The
5579                    // navigator's episodic path is §63.C's adaptive corpus
5580                    // reinforcement, keyed by the corpus declaration; the
5581                    // value is accepted and recorded on the seed so nothing
5582                    // is silently dropped, and the adaptive path already
5583                    // reads the corpus-level flag.
5584                    "recall" => {
5585                        let mode = self.consume_any_ident_or_kw()?.value.clone();
5586                        if node.seed.is_empty() {
5587                            node.seed = format!("recall:{mode}");
5588                        }
5589                    }
5590                    _ => self.skip_value(),
5591                }
5592            }
5593        }
5594        if self.check(TokenType::LBrace) {
5595            self.advance();
5596            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5597                let f = self.current().value.clone();
5598                self.advance();
5599                if self.check(TokenType::Colon) {
5600                    self.advance();
5601                    match f.as_str() {
5602                        "corpus" => {
5603                            node.corpus_name = self.consume_any_ident_or_kw()?.value.clone()
5604                        }
5605                        "query" => node.query_expr = self.parse_pix_value()?,
5606                        "trail" => {
5607                            let v = self.consume_any_ident_or_kw()?.value;
5608                            node.trail_enabled =
5609                                matches!(v.as_str(), "true" | "enabled" | "on");
5610                        }
5611                        "output" | "as" => {
5612                            node.output_name = self.consume_any_ident_or_kw()?.value.clone()
5613                        }
5614                        // §Fase 63.B — MDN corpus-graph navigation.
5615                        "from" => node.seed = self.consume_any_ident_or_kw()?.value.clone(),
5616                        "budget" => node.budget = self.parse_optional_int(),
5617                        // §Fase 66 (Q2) — column-scoped navigation: a raw filter
5618                        // expr (mirrors `retrieve … where`) pushed to the SELECT
5619                        // that sources the corpus `documents:`/`relations:` rows,
5620                        // so a `corpus from axonstore` is scoped to a sub-tenant
5621                        // COLUMN (`where: "tenant_id == '${tenant_id}'"`), not just
5622                        // the axon-tenant RLS scope. Resolved by the §37.d filter
5623                        // compiler at runtime (`${name}` → `$N` bind params).
5624                        "where" => {
5625                            node.where_expr = self.consume(TokenType::StringLit)?.value.clone()
5626                        }
5627                        _ => self.skip_value(),
5628                    }
5629                }
5630            }
5631            if self.check(TokenType::RBrace) {
5632                self.advance();
5633            }
5634        }
5635        Ok(FlowStep::Navigate(node))
5636    }
5637
5638    fn parse_drill_step(&mut self) -> Result<FlowStep, ParseError> {
5639        let tok = self.current().clone();
5640        self.advance();
5641        let pix_name = self.consume_any_ident_or_kw()?.value.clone();
5642        let mut node = DrillStep {
5643            pix_name,
5644            subtree_path: String::new(),
5645            query_expr: String::new(),
5646            output_name: String::new(),
5647            loc: Loc {
5648                line: tok.line,
5649                column: tok.column,
5650            },
5651        };
5652        // §Fase 119.f — `drill <Ref> into "<path>" query: … as: …`, the form
5653        // README publishes. `into` is a positional keyword (no colon), the
5654        // rest is the same braceless closed-catalog field list as `navigate`.
5655        if self.current().value == "into" {
5656            self.advance();
5657            // §Fase 119.f — README writes BOTH `into "Liabilities"` (a title)
5658            // and `into findings.top_region` (a dotted binding path). The
5659            // subtree path is dot-separated either way, so both spellings
5660            // land in the same field.
5661            node.subtree_path = if self.check(TokenType::StringLit) {
5662                self.consume(TokenType::StringLit)?.value.clone()
5663            } else {
5664                self.parse_dotted_identifier()?
5665            };
5666        }
5667        if !self.check(TokenType::LBrace) {
5668            while self.at_drill_field() {
5669                let f = self.current().value.clone();
5670                self.advance();
5671                self.consume(TokenType::Colon)?;
5672                match f.as_str() {
5673                    "subtree" | "path" => {
5674                        node.subtree_path = self.consume(TokenType::StringLit)?.value.clone()
5675                    }
5676                    "query" => node.query_expr = self.parse_pix_value()?,
5677                    "output" | "as" => {
5678                        node.output_name = self.consume_any_ident_or_kw()?.value.clone()
5679                    }
5680                    _ => self.skip_value(),
5681                }
5682            }
5683        }
5684        if self.check(TokenType::LBrace) {
5685            self.advance();
5686            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5687                let f = self.current().value.clone();
5688                self.advance();
5689                if self.check(TokenType::Colon) {
5690                    self.advance();
5691                    match f.as_str() {
5692                        "subtree" | "path" => {
5693                            node.subtree_path = self.consume(TokenType::StringLit)?.value.clone()
5694                        }
5695                        "query" => node.query_expr = self.parse_pix_value()?,
5696                        "output" | "as" => {
5697                            node.output_name = self.consume_any_ident_or_kw()?.value.clone()
5698                        }
5699                        _ => self.skip_value(),
5700                    }
5701                }
5702            }
5703            if self.check(TokenType::RBrace) {
5704                self.advance();
5705            }
5706        }
5707        Ok(FlowStep::Drill(node))
5708    }
5709
5710    fn parse_corroborate_step(&mut self) -> Result<FlowStep, ParseError> {
5711        let tok = self.current().clone();
5712        self.advance();
5713        let nav_ref = self.consume_any_ident_or_kw()?.value.clone();
5714        let mut output = String::new();
5715        if self.check(TokenType::Arrow) {
5716            self.advance();
5717            output = self.consume_any_ident_or_kw()?.value.clone();
5718        }
5719        Ok(FlowStep::Corroborate(CorroborateStep {
5720            navigate_ref: nav_ref,
5721            output_name: output,
5722            loc: Loc {
5723                line: tok.line,
5724                column: tok.column,
5725            },
5726        }))
5727    }
5728
5729    fn parse_listen_step(&mut self) -> Result<FlowStep, ParseError> {
5730        let tok = self.current().clone();
5731        self.advance();
5732        // §λ-L-E Fase 13 D4 — dual-mode listen:
5733        //   • String topic (legacy, deprecated since Fase 13)
5734        //   • Identifier (canonical: declared ChannelDefinition)
5735        let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
5736            (self.consume(TokenType::StringLit)?.value.clone(), false)
5737        } else {
5738            (self.consume_any_ident_or_kw()?.value.clone(), true)
5739        };
5740        let mut alias = String::new();
5741        if !self.at_declaration_start()
5742            && !self.check(TokenType::RBrace)
5743            && !self.check(TokenType::LBrace)
5744        {
5745            let next = self.current().clone();
5746            if next.value == "as" || next.ttype == TokenType::As {
5747                self.advance();
5748                alias = self.consume_any_ident_or_kw()?.value.clone();
5749            }
5750        }
5751        // §Fase 52.a — parse the handler body into real flow-steps (was
5752        // `skip_braced_block`'d, leaving the listener inert). The body runs on
5753        // each event / scheduled tick.
5754        let body = self.parse_listener_body()?;
5755        Ok(FlowStep::Listen(ListenStep {
5756            channel,
5757            channel_is_ref,
5758            event_alias: alias,
5759            body,
5760            loc: Loc {
5761                line: tok.line,
5762                column: tok.column,
5763            },
5764        }))
5765    }
5766
5767    /// §Fase 52.a — parse a `listen … { <flow steps> }` handler body. The body
5768    /// is OPTIONAL (a bodyless `listen channel` returns an empty Vec); when
5769    /// present, each statement is a real [`FlowStep`] (the same grammar as a
5770    /// flow / `quant` / `par` body), executed per trigger by the §52.c runtime.
5771    fn parse_listener_body(&mut self) -> Result<Vec<FlowStep>, ParseError> {
5772        let mut body = Vec::new();
5773        if self.check(TokenType::LBrace) {
5774            self.advance(); // consume `{`
5775            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5776                body.push(self.parse_flow_step()?);
5777            }
5778            self.consume(TokenType::RBrace)?;
5779        }
5780        Ok(body)
5781    }
5782
5783    fn parse_retrieve_step(&mut self) -> Result<FlowStep, ParseError> {
5784        let tok = self.current().clone();
5785        self.advance();
5786        let store = self.consume_any_ident_or_kw()?.value.clone();
5787        let mut where_expr = String::new();
5788        let mut alias = String::new();
5789        let mut order_by = String::new();
5790        let mut limit_expr = String::new();
5791        let mut aggregate = String::new();
5792        let mut group_by = String::new();
5793        let mut cache = String::new();
5794        if self.check(TokenType::LBrace) {
5795            self.advance();
5796            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5797                let f = self.current().value.clone();
5798                self.advance();
5799                if self.check(TokenType::Colon) {
5800                    self.advance();
5801                    match f.as_str() {
5802                        "where" => where_expr = self.consume(TokenType::StringLit)?.value.clone(),
5803                        "as" | "alias" => alias = self.consume_any_ident_or_kw()?.value.clone(),
5804                        // §Fase 67.b — `order_by:` is a string literal
5805                        // (`"col asc, col2 desc"`), same surface as `where:`.
5806                        "order_by" => {
5807                            order_by = self.consume(TokenType::StringLit)?.value.clone()
5808                        }
5809                        // §Fase 67.b — `limit:` is a bare integer literal
5810                        // (`limit: 100`) OR a string carrying a binding
5811                        // (`limit: "${max}"`). Captured raw; the runtime
5812                        // resolves + validates it as a `u32`.
5813                        "limit" => {
5814                            let t = self.current().clone();
5815                            match t.ttype {
5816                                TokenType::Integer | TokenType::StringLit => {
5817                                    limit_expr = t.value.clone();
5818                                    self.advance();
5819                                }
5820                                _ => self.skip_value(),
5821                            }
5822                        }
5823                        // §Fase 76.d — `aggregate:` is a string literal from
5824                        // the CLOSED catalog (`"count"`, `"sum(tokens)"`, …);
5825                        // `group_by:` is a string literal listing columns
5826                        // (`"industry, status"`). Both captured raw; the
5827                        // §38.d proof (axon-T843/T844/T845) + the runtime
5828                        // (`filter::parse_aggregate_clause`) validate.
5829                        "aggregate" => {
5830                            aggregate = self.consume(TokenType::StringLit)?.value.clone()
5831                        }
5832                        "group_by" => {
5833                            group_by = self.consume(TokenType::StringLit)?.value.clone()
5834                        }
5835                        // §Fase 85.b — `cache:` names a declared `cache`
5836                        // policy. A retrieve reads a store (never `pure`), so
5837                        // caching it always accepts staleness — the checker
5838                        // requires a finite `ttl:` on the referenced cache
5839                        // (axon-T865) and resolves the reference (axon-T864).
5840                        "cache" => cache = self.consume_any_ident_or_kw()?.value.clone(),
5841                        _ => self.skip_value(),
5842                    }
5843                }
5844            }
5845            if self.check(TokenType::RBrace) {
5846                self.advance();
5847            }
5848        }
5849        Ok(FlowStep::Retrieve(RetrieveStep {
5850            store_name: store,
5851            where_expr,
5852            alias,
5853            order_by,
5854            limit_expr,
5855            aggregate,
5856            group_by,
5857            cache,
5858            loc: Loc {
5859                line: tok.line,
5860                column: tok.column,
5861            },
5862        }))
5863    }
5864
5865    /// §Fase 35.m — Parse a `purge` step, capturing the optional
5866    /// `{ where: "<expr>" }` filter. (Fase 35.p moved `mutate` to its
5867    /// own `parse_mutate_step`, which also captures SET columns; this
5868    /// helper now serves `purge` alone — a `DELETE` has no SET clause.)
5869    ///
5870    /// Before Fase 35.m these two steps parsed via `parse_flow_step_simple`,
5871    /// which *skipped* the braced block — so a written `where:` clause
5872    /// was silently dropped and every `mutate`/`purge` ran against the
5873    /// whole store, leaving the entire Fase 35.b/c parameterized-filter
5874    /// machinery unreachable for them. This mirror of `parse_retrieve_step`
5875    /// (minus the `as:` alias — a mutate/purge binds no result) closes
5876    /// that gap. Returns `(loc, store_name, where_expr)`.
5877    fn parse_store_where_step(
5878        &mut self,
5879    ) -> Result<(Loc, String, String), ParseError> {
5880        let tok = self.current().clone();
5881        self.advance(); // consume the keyword
5882        let store = if self.at_declaration_start()
5883            || self.check(TokenType::RBrace)
5884            || self.check(TokenType::Eof)
5885        {
5886            String::new()
5887        } else {
5888            self.consume_any_ident_or_kw()?.value.clone()
5889        };
5890        let mut where_expr = String::new();
5891        if self.check(TokenType::LBrace) {
5892            self.advance();
5893            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5894                let field = self.current().value.clone();
5895                self.advance();
5896                if self.check(TokenType::Colon) {
5897                    self.advance();
5898                    match field.as_str() {
5899                        "where" => {
5900                            where_expr =
5901                                self.consume(TokenType::StringLit)?.value.clone()
5902                        }
5903                        _ => self.skip_value(),
5904                    }
5905                }
5906            }
5907            if self.check(TokenType::RBrace) {
5908                self.advance();
5909            }
5910        }
5911        Ok((
5912            Loc {
5913                line: tok.line,
5914                column: tok.column,
5915            },
5916            store,
5917            where_expr,
5918        ))
5919    }
5920
5921    /// §Fase 35.o — Parse a `persist` step, capturing the optional
5922    /// `{ col: value }` field block.
5923    ///
5924    /// Before Fase 35.o `persist` parsed via `parse_flow_step_simple`,
5925    /// which *skipped* the braced block — so a written field block was
5926    /// silently dropped and the runtime fell back to writing every
5927    /// context binding as a row, which fails against any real table
5928    /// (flows always carry more bindings than a table has columns).
5929    /// This captures the declared columns into `PersistStep.fields`;
5930    /// the runtime writes exactly those (interpolated). A `persist`
5931    /// with no block keeps the v1.30.0 user-bindings fallback — fully
5932    /// backward-compatible. Mirror of `parse_retrieve_step`, but the
5933    /// keys are arbitrary column names rather than the fixed
5934    /// `where:` / `as:` filter keys.
5935    ///
5936    /// The optional `into` connector (`persist into <store>`) is
5937    /// accepted and skipped — before Fase 35.o `into` was captured as
5938    /// the store name.
5939    fn parse_persist_step(&mut self) -> Result<FlowStep, ParseError> {
5940        let tok = self.current().clone();
5941        self.advance(); // consume `persist`
5942        // Optional `into` connector — skip it so the store name that
5943        // follows is not mistaken for the target.
5944        if self.current().value == "into" && !self.check(TokenType::LBrace) {
5945            self.advance();
5946        }
5947        let store = if self.at_declaration_start()
5948            || self.check(TokenType::LBrace)
5949            || self.check(TokenType::RBrace)
5950            || self.check(TokenType::Eof)
5951        {
5952            String::new()
5953        } else {
5954            self.consume_any_ident_or_kw()?.value.clone()
5955        };
5956        let mut fields: Vec<(String, String)> = Vec::new();
5957        if self.check(TokenType::LBrace) {
5958            self.advance();
5959            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5960                let col = self.current().value.clone();
5961                self.advance();
5962                if self.check(TokenType::Colon) {
5963                    self.advance();
5964                    let value = if self.check(TokenType::StringLit) {
5965                        self.consume(TokenType::StringLit)?.value.clone()
5966                    } else if self.check(TokenType::RBrace)
5967                        || self.check(TokenType::Eof)
5968                        || self.check(TokenType::Colon)
5969                    {
5970                        String::new()
5971                    } else {
5972                        let v = self.current().clone();
5973                        self.advance();
5974                        v.value.clone()
5975                    };
5976                    fields.push((col, value));
5977                }
5978            }
5979            if self.check(TokenType::RBrace) {
5980                self.advance();
5981            }
5982        }
5983        Ok(FlowStep::Persist(PersistStep {
5984            store_name: store,
5985            fields,
5986            loc: Loc {
5987                line: tok.line,
5988                column: tok.column,
5989            },
5990        }))
5991    }
5992
5993    /// §Fase 35.p — Parse a `mutate` step, capturing both the
5994    /// `{ where: "<expr>" }` filter AND the `{ col: value }` SET
5995    /// assignments.
5996    ///
5997    /// Before Fase 35.p `mutate` parsed via `parse_store_where_step`,
5998    /// which captured only `where:` and *skipped* every other key — so
5999    /// the runtime built the `UPDATE … SET` clause from every flow
6000    /// binding (params + step results + `let`s), which fails against
6001    /// any real table (`column "X" does not exist`). This closes the
6002    /// gap symmetrically to 35.o's `persist` block: every key other
6003    /// than `where:` is a SET column; a `mutate` with no SET column
6004    /// keeps the v1.31.0 user-bindings fallback. `where:` keeps its
6005    /// string-literal grammar (as in `retrieve` / `purge`).
6006    fn parse_mutate_step(&mut self) -> Result<FlowStep, ParseError> {
6007        let tok = self.current().clone();
6008        self.advance(); // consume `mutate`
6009        let store = if self.at_declaration_start()
6010            || self.check(TokenType::LBrace)
6011            || self.check(TokenType::RBrace)
6012            || self.check(TokenType::Eof)
6013        {
6014            String::new()
6015        } else {
6016            self.consume_any_ident_or_kw()?.value.clone()
6017        };
6018        let mut where_expr = String::new();
6019        let mut fields: Vec<(String, String)> = Vec::new();
6020        if self.check(TokenType::LBrace) {
6021            self.advance();
6022            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6023                let key = self.current().value.clone();
6024                self.advance();
6025                if self.check(TokenType::Colon) {
6026                    self.advance();
6027                    if key == "where" {
6028                        where_expr =
6029                            self.consume(TokenType::StringLit)?.value.clone();
6030                    } else {
6031                        let value = if self.check(TokenType::StringLit) {
6032                            self.consume(TokenType::StringLit)?.value.clone()
6033                        } else if self.check(TokenType::RBrace)
6034                            || self.check(TokenType::Eof)
6035                            || self.check(TokenType::Colon)
6036                        {
6037                            String::new()
6038                        } else {
6039                            let v = self.current().clone();
6040                            self.advance();
6041                            v.value.clone()
6042                        };
6043                        fields.push((key, value));
6044                    }
6045                }
6046            }
6047            if self.check(TokenType::RBrace) {
6048                self.advance();
6049            }
6050        }
6051        Ok(FlowStep::Mutate(MutateStep {
6052            store_name: store,
6053            where_expr,
6054            fields,
6055            loc: Loc {
6056                line: tok.line,
6057                column: tok.column,
6058            },
6059        }))
6060    }
6061
6062    // ── TIER 2 DECLARATIONS ────────────────────────────────────────
6063
6064    fn parse_agent(&mut self) -> Result<AgentDefinition, ParseError> {
6065        let tok = self.consume(TokenType::Agent)?;
6066        let name = self.consume(TokenType::Identifier)?.value;
6067        let mut node = AgentDefinition {
6068            name,
6069            goal: String::new(),
6070            tools: Vec::new(),
6071            memory_ref: String::new(),
6072            strategy: String::new(),
6073            on_stuck: String::new(),
6074            shield_ref: String::new(),
6075            max_iterations: None,
6076            max_tokens: None,
6077            max_time: String::new(),
6078            max_cost: None,
6079            loc: Loc {
6080                line: tok.line,
6081                column: tok.column,
6082            },
6083            leading_trivia: Vec::new(),
6084            trailing_trivia: Vec::new(),
6085        };
6086        // Skip optional parameters/return type before brace
6087        while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
6088            self.advance();
6089        }
6090        self.consume(TokenType::LBrace)?;
6091        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6092            let field = self.current().clone();
6093            let field_name = field.value.clone();
6094            self.advance();
6095            if self.check(TokenType::Colon) {
6096                self.advance();
6097                match field_name.as_str() {
6098                    "goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
6099                    "tools" => node.tools = self.parse_bracketed_identifiers()?,
6100                    "memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
6101                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
6102                    "on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
6103                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
6104                    "max_iterations" => node.max_iterations = self.parse_optional_int(),
6105                    "max_tokens" => node.max_tokens = self.parse_optional_int(),
6106                    "max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
6107                    "max_cost" => node.max_cost = self.parse_optional_float(),
6108                    _ => self.skip_value(),
6109                }
6110            } else if self.check(TokenType::LBrace) {
6111                self.skip_braced_block()?;
6112            }
6113        }
6114        self.consume(TokenType::RBrace)?;
6115        Ok(node)
6116    }
6117
6118    /// §Fase 53 — `extension Name { category: effects|scan, members: [ … ] }`.
6119    /// The parser is permissive on field/category VALUES (validated in
6120    /// §53.c by the type-checker — no-shadowing, category-membership);
6121    /// it only enforces the structural grammar here.
6122    fn parse_extension(&mut self) -> Result<ExtensionDefinition, ParseError> {
6123        let tok = self.consume(TokenType::Extension)?;
6124        let name = self.consume(TokenType::Identifier)?.value;
6125        let mut node = ExtensionDefinition {
6126            name,
6127            category: String::new(),
6128            members: Vec::new(),
6129            loc: Loc {
6130                line: tok.line,
6131                column: tok.column,
6132            },
6133            leading_trivia: Vec::new(),
6134            trailing_trivia: Vec::new(),
6135        };
6136        self.consume(TokenType::LBrace)?;
6137        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6138            let field_name = self.current().value.clone();
6139            self.advance();
6140            if self.check(TokenType::Colon) {
6141                self.advance();
6142                match field_name.as_str() {
6143                    "category" => {
6144                        node.category = self.consume_any_ident_or_kw()?.value.clone()
6145                    }
6146                    "members" => node.members = self.parse_extension_members()?,
6147                    _ => self.skip_value(),
6148                }
6149            } else if self.check(TokenType::LBrace) {
6150                self.skip_braced_block()?;
6151            }
6152        }
6153        self.consume(TokenType::RBrace)?;
6154        Ok(node)
6155    }
6156
6157    /// §Fase 53 — parse `[ "name" [ : { semantics: "…", default_confidence: 0.8 } ], … ]`.
6158    /// Each member is a string literal optionally followed by a metadata
6159    /// block. Trailing/interleaved commas are tolerated.
6160    fn parse_extension_members(&mut self) -> Result<Vec<ExtensionMember>, ParseError> {
6161        let mut members = Vec::new();
6162        self.consume(TokenType::LBracket)?;
6163        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
6164            let name_tok = self.consume(TokenType::StringLit)?;
6165            let mut member = ExtensionMember {
6166                name: name_tok.value.clone(),
6167                semantics: None,
6168                default_confidence: None,
6169                loc: Loc {
6170                    line: name_tok.line,
6171                    column: name_tok.column,
6172                },
6173            };
6174            // Optional `: { semantics: "…", default_confidence: 0.8 }`.
6175            if self.check(TokenType::Colon) {
6176                self.advance();
6177                self.consume(TokenType::LBrace)?;
6178                while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6179                    let mkey = self.current().value.clone();
6180                    self.advance();
6181                    if self.check(TokenType::Colon) {
6182                        self.advance();
6183                        match mkey.as_str() {
6184                            "semantics" => {
6185                                member.semantics =
6186                                    Some(self.consume(TokenType::StringLit)?.value.clone())
6187                            }
6188                            "default_confidence" => {
6189                                member.default_confidence = self.parse_optional_float()
6190                            }
6191                            _ => self.skip_value(),
6192                        }
6193                    }
6194                    if self.check(TokenType::Comma) {
6195                        self.advance();
6196                    }
6197                }
6198                self.consume(TokenType::RBrace)?;
6199            }
6200            members.push(member);
6201            if self.check(TokenType::Comma) {
6202                self.advance();
6203            }
6204        }
6205        self.consume(TokenType::RBracket)?;
6206        Ok(members)
6207    }
6208
6209    /// §Fase 71.a/e — `window <Name> { timezone: "…"  allow: [ {days hours} ]
6210    /// exclude: [ "YYYY-MM-DD", … ]  on_outside: skip|defer|warn }`.
6211    fn parse_window(&mut self) -> Result<WindowDefinition, ParseError> {
6212        let tok = self.consume(TokenType::Window)?;
6213        let name = self.consume(TokenType::Identifier)?.value;
6214        let mut node = WindowDefinition {
6215            name,
6216            timezone: String::new(),
6217            allow: Vec::new(),
6218            exclude: Vec::new(),
6219            on_outside: String::new(),
6220            loc: Loc {
6221                line: tok.line,
6222                column: tok.column,
6223            },
6224            leading_trivia: Vec::new(),
6225            trailing_trivia: Vec::new(),
6226        };
6227        self.consume(TokenType::LBrace)?;
6228        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6229            let field_name = self.consume_any_ident_or_kw()?.value;
6230            self.consume(TokenType::Colon)?;
6231            match field_name.as_str() {
6232                "timezone" => node.timezone = self.consume(TokenType::StringLit)?.value,
6233                "allow" => node.allow = self.parse_window_allow()?,
6234                "exclude" => node.exclude = self.parse_window_exclude()?,
6235                "on_outside" => node.on_outside = self.consume_any_ident_or_kw()?.value,
6236                _ => self.skip_value(),
6237            }
6238        }
6239        self.consume(TokenType::RBrace)?;
6240        Ok(node)
6241    }
6242
6243    /// §Fase 71.a — the `allow: [ { … }, { … } ]` span list.
6244    fn parse_window_allow(&mut self) -> Result<Vec<WindowSpan>, ParseError> {
6245        self.consume(TokenType::LBracket)?;
6246        let mut spans = Vec::new();
6247        if !self.check(TokenType::RBracket) {
6248            spans.push(self.parse_window_span()?);
6249            while self.check(TokenType::Comma) {
6250                self.advance();
6251                if self.check(TokenType::RBracket) {
6252                    break; // trailing comma
6253                }
6254                spans.push(self.parse_window_span()?);
6255            }
6256        }
6257        self.consume(TokenType::RBracket)?;
6258        Ok(spans)
6259    }
6260
6261    /// §Fase 71.e — the `exclude: [ "YYYY-MM-DD", … ]` holiday list (ISO
6262    /// date-string literals; validated for real-calendar-date-ness by the
6263    /// `axon-T826` type check). An empty list / absent field ⇒ no holidays.
6264    fn parse_window_exclude(&mut self) -> Result<Vec<String>, ParseError> {
6265        self.consume(TokenType::LBracket)?;
6266        let mut dates = Vec::new();
6267        if !self.check(TokenType::RBracket) {
6268            dates.push(self.consume(TokenType::StringLit)?.value);
6269            while self.check(TokenType::Comma) {
6270                self.advance();
6271                if self.check(TokenType::RBracket) {
6272                    break; // trailing comma
6273                }
6274                dates.push(self.consume(TokenType::StringLit)?.value);
6275            }
6276        }
6277        self.consume(TokenType::RBracket)?;
6278        Ok(dates)
6279    }
6280
6281    /// §Fase 71.a — one span `{ days: Mon..Fri  hours: 9..18 }`.
6282    fn parse_window_span(&mut self) -> Result<WindowSpan, ParseError> {
6283        let tok = self.consume(TokenType::LBrace)?;
6284        let mut span = WindowSpan {
6285            day_start: String::new(),
6286            day_end: String::new(),
6287            hour_start: 0,
6288            hour_end: 0,
6289            loc: Loc {
6290                line: tok.line,
6291                column: tok.column,
6292            },
6293        };
6294        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6295            let field = self.consume_any_ident_or_kw()?.value;
6296            self.consume(TokenType::Colon)?;
6297            match field.as_str() {
6298                "days" => {
6299                    span.day_start = self.consume_any_ident_or_kw()?.value;
6300                    self.consume(TokenType::DotDot)?;
6301                    span.day_end = self.consume_any_ident_or_kw()?.value;
6302                }
6303                "hours" => {
6304                    span.hour_start = self.consume_number()? as i64;
6305                    self.consume(TokenType::DotDot)?;
6306                    span.hour_end = self.consume_number()? as i64;
6307                }
6308                _ => self.skip_value(),
6309            }
6310            if self.check(TokenType::Comma) {
6311                self.advance();
6312            }
6313        }
6314        self.consume(TokenType::RBrace)?;
6315        Ok(span)
6316    }
6317
6318    fn parse_shield(&mut self) -> Result<ShieldDefinition, ParseError> {
6319        let tok = self.consume(TokenType::Shield)?;
6320        let name = self.consume(TokenType::Identifier)?.value;
6321        let mut node = ShieldDefinition {
6322            name,
6323            scan: Vec::new(),
6324            strategy: String::new(),
6325            on_breach: String::new(),
6326            severity: String::new(),
6327            quarantine: String::new(),
6328            max_retries: None,
6329            confidence_threshold: None,
6330            allow_tools: Vec::new(),
6331            deny_tools: Vec::new(),
6332            sandbox: None,
6333            redact: Vec::new(),
6334            log: String::new(),
6335            deflect_message: String::new(),
6336            taint: String::new(),
6337            compliance: Vec::new(),
6338            sign: String::new(),
6339            unknown_fields: Vec::new(),
6340            loc: Loc {
6341                line: tok.line,
6342                column: tok.column,
6343            },
6344            leading_trivia: Vec::new(),
6345            trailing_trivia: Vec::new(),
6346        };
6347        self.consume(TokenType::LBrace)?;
6348        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6349            let field_name = self.current().value.clone();
6350            let field_loc = Loc {
6351                line: self.current().line,
6352                column: self.current().column,
6353            };
6354            self.advance();
6355            if self.check(TokenType::Colon) {
6356                self.advance();
6357                match field_name.as_str() {
6358                    "scan" => node.scan = self.parse_bracketed_identifiers()?,
6359                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
6360                    "on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
6361                    "severity" => node.severity = self.consume_any_ident_or_kw()?.value.clone(),
6362                    "quarantine" => {
6363                        node.quarantine = self.consume(TokenType::StringLit)?.value.clone()
6364                    }
6365                    "max_retries" => node.max_retries = self.parse_optional_int(),
6366                    "confidence_threshold" => {
6367                        node.confidence_threshold = self.parse_optional_float()
6368                    }
6369                    "allow_tools" => node.allow_tools = self.parse_bracketed_identifiers()?,
6370                    "deny_tools" => node.deny_tools = self.parse_bracketed_identifiers()?,
6371                    "sandbox" => {
6372                        node.sandbox = Some(self.consume_any_ident_or_kw()?.value == "true")
6373                    }
6374                    "redact" => node.redact = self.parse_bracketed_identifiers()?,
6375                    "log" => node.log = self.consume_any_ident_or_kw()?.value.clone(),
6376                    "deflect_message" => {
6377                        node.deflect_message = self.consume(TokenType::StringLit)?.value.clone()
6378                    }
6379                    "taint" => node.taint = self.consume_any_ident_or_kw()?.value.clone(),
6380                    // ESK Fase 6.1 — covered regulatory classes.
6381                    "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
6382                    // §Fase 77.a — egress signing algorithm (closed catalog,
6383                    // validated by the checker: `axon-T846`).
6384                    "sign" => node.sign = self.consume_any_ident_or_kw()?.value.clone(),
6385                    // §Fase 77.a — the value is still skipped (leniency
6386                    // preserved) but the NAME is recorded so the checker
6387                    // emits `axon-W010` instead of a silent drop.
6388                    _ => {
6389                        node.unknown_fields.push((field_name.clone(), field_loc));
6390                        self.skip_value()
6391                    }
6392                }
6393            } else if self.check(TokenType::LBrace) {
6394                self.skip_braced_block()?;
6395            }
6396        }
6397        self.consume(TokenType::RBrace)?;
6398        Ok(node)
6399    }
6400
6401    fn parse_pix(&mut self) -> Result<PixDefinition, ParseError> {
6402        let tok = self.consume(TokenType::Pix)?;
6403        let name = self.consume(TokenType::Identifier)?.value;
6404        let mut node = PixDefinition {
6405            name,
6406            source: String::new(),
6407            depth: None,
6408            branching: None,
6409            model: String::new(),
6410            loc: Loc {
6411                line: tok.line,
6412                column: tok.column,
6413            },
6414            leading_trivia: Vec::new(),
6415            trailing_trivia: Vec::new(),
6416        };
6417        self.consume(TokenType::LBrace)?;
6418        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6419            let field_name = self.current().value.clone();
6420            self.advance();
6421            if self.check(TokenType::Colon) {
6422                self.advance();
6423                match field_name.as_str() {
6424                    "source" => node.source = self.consume(TokenType::StringLit)?.value.clone(),
6425                    "depth" => node.depth = self.parse_optional_int(),
6426                    "branching" => node.branching = self.parse_optional_int(),
6427                    "model" => node.model = self.consume_any_ident_or_kw()?.value.clone(),
6428                    _ => self.skip_value(),
6429                }
6430            } else if self.check(TokenType::LBrace) {
6431                self.skip_braced_block()?;
6432            }
6433        }
6434        self.consume(TokenType::RBrace)?;
6435        Ok(node)
6436    }
6437
6438    /// §Fase 62.0 — `ledger <Name> { source, depth, branching, model }`.
6439    /// The append-only audit chain (formerly the Provenance-Index reading of
6440    /// `pix`). Field grammar mirrors `pix` (same shape) but the SEMANTICS are
6441    /// audit, not navigation: `depth` = chain retention, `branching` = Merkle
6442    /// factor, `model` = hash slug (sha256 / blake3 / sha3).
6443    fn parse_ledger(&mut self) -> Result<LedgerDefinition, ParseError> {
6444        let tok = self.consume(TokenType::Ledger)?;
6445        let name = self.consume(TokenType::Identifier)?.value;
6446        let mut node = LedgerDefinition {
6447            name,
6448            source: String::new(),
6449            depth: None,
6450            branching: None,
6451            model: String::new(),
6452            loc: Loc {
6453                line: tok.line,
6454                column: tok.column,
6455            },
6456            leading_trivia: Vec::new(),
6457            trailing_trivia: Vec::new(),
6458        };
6459        self.consume(TokenType::LBrace)?;
6460        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6461            let field_name = self.current().value.clone();
6462            self.advance();
6463            if self.check(TokenType::Colon) {
6464                self.advance();
6465                match field_name.as_str() {
6466                    "source" => node.source = self.consume(TokenType::StringLit)?.value.clone(),
6467                    "depth" => node.depth = self.parse_optional_int(),
6468                    "branching" => node.branching = self.parse_optional_int(),
6469                    "model" => node.model = self.consume_any_ident_or_kw()?.value.clone(),
6470                    _ => self.skip_value(),
6471                }
6472            } else if self.check(TokenType::LBrace) {
6473                self.skip_braced_block()?;
6474            }
6475        }
6476        self.consume(TokenType::RBrace)?;
6477        Ok(node)
6478    }
6479
6480    fn parse_psyche(&mut self) -> Result<PsycheDefinition, ParseError> {
6481        let tok = self.consume(TokenType::Psyche)?;
6482        let name = self.consume(TokenType::Identifier)?.value;
6483        let mut node = PsycheDefinition {
6484            name,
6485            dimensions: Vec::new(),
6486            manifold_noise: None,
6487            manifold_momentum: None,
6488            safety_constraints: Vec::new(),
6489            quantum_enabled: None,
6490            inference_mode: String::new(),
6491            loc: Loc {
6492                line: tok.line,
6493                column: tok.column,
6494            },
6495            leading_trivia: Vec::new(),
6496            trailing_trivia: Vec::new(),
6497        };
6498        self.consume(TokenType::LBrace)?;
6499        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6500            let field_name = self.current().value.clone();
6501            self.advance();
6502            if self.check(TokenType::Colon) {
6503                self.advance();
6504                match field_name.as_str() {
6505                    "dimensions" => node.dimensions = self.parse_bracketed_identifiers()?,
6506                    "manifold_noise" => node.manifold_noise = self.parse_optional_float(),
6507                    "manifold_momentum" => node.manifold_momentum = self.parse_optional_float(),
6508                    // §Fase 119.f — `safety:` is what README §psyche publishes;
6509                    // `safety_constraints:` is what the parser has always taken.
6510                    // One field, two spellings — the `epsilon`/`tolerance`
6511                    // resolution of §119.b.1.
6512                    "safety_constraints" | "safety" => {
6513                        node.safety_constraints = self.parse_bracketed_identifiers()?
6514                    }
6515                    "quantum_enabled" => {
6516                        node.quantum_enabled = Some(self.consume_any_ident_or_kw()?.value == "true")
6517                    }
6518                    "inference_mode" => {
6519                        node.inference_mode = self.consume_any_ident_or_kw()?.value.clone()
6520                    }
6521                    _ => self.skip_value(),
6522                }
6523            } else if self.check(TokenType::LBrace) {
6524                self.skip_braced_block()?;
6525            }
6526        }
6527        self.consume(TokenType::RBrace)?;
6528        Ok(node)
6529    }
6530
6531    fn parse_corpus(&mut self) -> Result<CorpusDefinition, ParseError> {
6532        let tok = self.consume(TokenType::Corpus)?;
6533        let name = self.consume(TokenType::Identifier)?.value;
6534        let mut node = CorpusDefinition {
6535            name,
6536            documents: Vec::new(),
6537            relations: Vec::new(),
6538            adaptive: false,
6539            mcp_server: String::new(),
6540            mcp_resource_uri: String::new(),
6541            store_source: None,
6542            loc: Loc {
6543                line: tok.line,
6544                column: tok.column,
6545            },
6546            leading_trivia: Vec::new(),
6547            trailing_trivia: Vec::new(),
6548        };
6549        // corpus Name from mcp("server", "uri")  — static MCP-bound short form.
6550        // corpus Name from axonstore { documents: S(id,title)  relations: … }  —
6551        // §Fase 64.A dynamic store-sourced MDN graph (falls through to the body).
6552        let mut dynamic = false;
6553        if self.check(TokenType::From) {
6554            self.advance();
6555            if self.check(TokenType::AxonStore) {
6556                self.advance();
6557                dynamic = true;
6558            } else {
6559                self.consume(TokenType::Mcp)?;
6560                self.consume(TokenType::LParen)?;
6561                node.mcp_server = self.consume(TokenType::StringLit)?.value.clone();
6562                self.consume(TokenType::Comma)?;
6563                node.mcp_resource_uri = self.consume(TokenType::StringLit)?.value.clone();
6564                self.consume(TokenType::RParen)?;
6565                return Ok(node);
6566            }
6567        }
6568        self.consume(TokenType::LBrace)?;
6569        // §Fase 64.A — accumulate the store-mapping pieces while the dynamic body
6570        // is parsed; folded into `node.store_source` after the closing brace.
6571        let mut src = CorpusStoreSource {
6572            doc_store: String::new(),
6573            doc_id_col: String::new(),
6574            doc_title_col: String::new(),
6575            edge_store: String::new(),
6576            edge_from_col: String::new(),
6577            edge_to_col: String::new(),
6578            edge_type_col: String::new(),
6579            edge_weight_col: String::new(),
6580            loc: Loc {
6581                line: tok.line,
6582                column: tok.column,
6583            },
6584        };
6585        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6586            let field_name = self.current().value.clone();
6587            self.advance();
6588            if self.check(TokenType::Colon) {
6589                self.advance();
6590                match field_name.as_str() {
6591                    // §Fase 64.A — dynamic: `documents: <DocStore>(id_col, title_col)`.
6592                    "documents" if dynamic => {
6593                        let (store, cols) = self.parse_corpus_store_mapping(2)?;
6594                        src.doc_store = store;
6595                        src.doc_id_col = cols[0].clone();
6596                        src.doc_title_col = cols[1].clone();
6597                    }
6598                    "documents" => node.documents = self.parse_bracketed_identifiers()?,
6599                    // §Fase 64.A — dynamic: `relations: <EdgeStore>(from, to, etype, weight)`.
6600                    "relations" if dynamic => {
6601                        let (store, cols) = self.parse_corpus_store_mapping(4)?;
6602                        src.edge_store = store;
6603                        src.edge_from_col = cols[0].clone();
6604                        src.edge_to_col = cols[1].clone();
6605                        src.edge_type_col = cols[2].clone();
6606                        src.edge_weight_col = cols[3].clone();
6607                    }
6608                    // §Fase 63.A — static typed weighted edges → MDN corpus graph.
6609                    "relations" => node.relations = self.parse_corpus_relations()?,
6610                    // §Fase 63.C — enable the memory endofunctor.
6611                    "adaptive" => node.adaptive = self.consume_any_ident_or_kw()?.value == "true",
6612                    _ => self.skip_value(),
6613                }
6614            } else if self.check(TokenType::LBrace) {
6615                self.skip_braced_block()?;
6616            }
6617        }
6618        self.consume(TokenType::RBrace)?;
6619        if dynamic {
6620            node.store_source = Some(src);
6621        }
6622        Ok(node)
6623    }
6624
6625    /// §Fase 64.A — parse a store-mapping `<StoreName>( col1, col2, … )` of exactly
6626    /// `n` columns. Used by the dynamic store-sourced corpus's `documents:` (2
6627    /// cols: id, title) and `relations:` (4 cols: from, to, etype, weight). The
6628    /// store name is an identifier (a declared `axonstore`); the columns may be
6629    /// keywords (a column could be named `from`/`type`), so they use the
6630    /// keyword-tolerant consumer. The type-checker validates store + columns.
6631    fn parse_corpus_store_mapping(&mut self, n: usize) -> Result<(String, Vec<String>), ParseError> {
6632        let store = self.consume(TokenType::Identifier)?.value.clone();
6633        self.consume(TokenType::LParen)?;
6634        let mut cols = Vec::with_capacity(n);
6635        for i in 0..n {
6636            if i > 0 {
6637                self.consume(TokenType::Comma)?;
6638            }
6639            cols.push(self.consume_any_ident_or_kw()?.value.clone());
6640        }
6641        self.consume(TokenType::RParen)?;
6642        Ok((store, cols))
6643    }
6644
6645    /// §Fase 63.A — parse `relations: [ etype(from, to, weight) … ]`, the typed
6646    /// weighted edges of an MDN corpus graph. Entries are whitespace/newline
6647    /// separated; commas between them are optional. Edge-type validity (closed
6648    /// catalog), document references, and the weight range are checked by the
6649    /// type-checker (`check_corpus`), not here.
6650    fn parse_corpus_relations(&mut self) -> Result<Vec<CorpusRelation>, ParseError> {
6651        let mut out = Vec::new();
6652        self.consume(TokenType::LBracket)?;
6653        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
6654            if self.check(TokenType::Comma) {
6655                self.advance();
6656                continue;
6657            }
6658            let tok = self.current().clone();
6659            let etype = self.consume_any_ident_or_kw()?.value.clone();
6660            self.consume(TokenType::LParen)?;
6661            let from = self.consume_any_ident_or_kw()?.value.clone();
6662            self.consume(TokenType::Comma)?;
6663            let to = self.consume_any_ident_or_kw()?.value.clone();
6664            self.consume(TokenType::Comma)?;
6665            let weight = self.consume_number()?;
6666            self.consume(TokenType::RParen)?;
6667            out.push(CorpusRelation {
6668                etype,
6669                from,
6670                to,
6671                weight,
6672                loc: Loc { line: tok.line, column: tok.column },
6673            });
6674        }
6675        self.consume(TokenType::RBracket)?;
6676        Ok(out)
6677    }
6678
6679    /// §Fase 108.b — the typed dataspace declaration:
6680    ///
6681    /// ```text
6682    /// dataspace <Name> {
6683    ///     column <name>: <Type>
6684    ///     …
6685    /// }
6686    /// ```
6687    ///
6688    /// Until 108.b the body was consumed by `skip_braced_block()` — any
6689    /// content, including garbage, compiled clean and reached nothing.
6690    /// Now each entry must be a `column` field; the declared type is
6691    /// kept RAW here and resolved against the closed 6-type catalog by
6692    /// the type-checker (`axon-T928`), so all schema errors accumulate
6693    /// in a single compile. An unknown body keyword is a parse error
6694    /// (the grammar is closed — the §38 axonstore posture).
6695    fn parse_dataspace(&mut self) -> Result<DataspaceDefinition, ParseError> {
6696        let tok = self.consume(TokenType::Dataspace)?;
6697        let name = self.consume(TokenType::Identifier)?.value;
6698        let mut node = DataspaceDefinition {
6699            name,
6700            columns: Vec::new(),
6701            loc: Loc {
6702                line: tok.line,
6703                column: tok.column,
6704            },
6705            leading_trivia: Vec::new(),
6706            trailing_trivia: Vec::new(),
6707        };
6708        if self.check(TokenType::LBrace) {
6709            self.consume(TokenType::LBrace)?;
6710            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6711                let entry = self.current().clone();
6712                if entry.value != "column" {
6713                    return Err(ParseError {
6714                        message: format!(
6715                            "Unknown entry `{}` in dataspace `{}`. A dataspace body \
6716                             declares its columnar schema: `column <name>: <Type>` \
6717                             (one per line, over the closed type catalog — \
6718                             Text, Int, Float, Bool, Timestamp, Json).",
6719                            entry.value, node.name
6720                        ),
6721                        line: entry.line,
6722                        column: entry.column,
6723                        ..Default::default()
6724                    });
6725                }
6726                self.advance(); // `column`
6727                let col_tok = self.current().clone();
6728                let col_name = self.consume_any_ident_or_kw()?.value.clone();
6729                self.consume(TokenType::Colon)?;
6730                let declared_type = self.consume_any_ident_or_kw()?.value.clone();
6731                node.columns.push(crate::ast::DataspaceColumn {
6732                    name: col_name,
6733                    declared_type,
6734                    loc: Loc {
6735                        line: col_tok.line,
6736                        column: col_tok.column,
6737                    },
6738                });
6739            }
6740            self.consume(TokenType::RBrace)?;
6741        }
6742        Ok(node)
6743    }
6744
6745    fn parse_ots(&mut self) -> Result<OtsDefinition, ParseError> {
6746        let tok = self.consume(TokenType::Ots)?;
6747        let name = self.consume(TokenType::Identifier)?.value;
6748        let mut node = OtsDefinition {
6749            name,
6750            teleology: String::new(),
6751            homotopy_search: String::new(),
6752            loss_function: String::new(),
6753            loc: Loc {
6754                line: tok.line,
6755                column: tok.column,
6756            },
6757            leading_trivia: Vec::new(),
6758            trailing_trivia: Vec::new(),
6759        };
6760        // Skip optional type params <In, Out>
6761        if self.check(TokenType::Lt) {
6762            while !self.check(TokenType::Gt) && !self.check(TokenType::Eof) {
6763                self.advance();
6764            }
6765            if self.check(TokenType::Gt) {
6766                self.advance();
6767            }
6768        }
6769        self.consume(TokenType::LBrace)?;
6770        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6771            let field_name = self.current().value.clone();
6772            self.advance();
6773            if self.check(TokenType::Colon) {
6774                self.advance();
6775                match field_name.as_str() {
6776                    "teleology" => {
6777                        node.teleology = self.consume(TokenType::StringLit)?.value.clone()
6778                    }
6779                    "homotopy_search" => {
6780                        node.homotopy_search = self.consume_any_ident_or_kw()?.value.clone()
6781                    }
6782                    // §Fase 119.c — README's ots blocks write the loss as a bare
6783                    // identifier (`loss_function: SemanticPreservation`, `L2`,
6784                    // `Contrastive`); the parser accepted only a string literal, so
6785                    // all three published blocks failed at this exact token. Both
6786                    // spellings resolve to the same field.
6787                    "loss_function" => {
6788                        node.loss_function = if self.check(TokenType::StringLit) {
6789                            self.consume(TokenType::StringLit)?.value.clone()
6790                        } else {
6791                            self.consume_any_ident_or_kw()?.value.clone()
6792                        }
6793                    }
6794                    _ => self.skip_value(),
6795                }
6796            } else if self.check(TokenType::LBrace) {
6797                self.skip_braced_block()?;
6798            }
6799        }
6800        self.consume(TokenType::RBrace)?;
6801        Ok(node)
6802    }
6803
6804    fn parse_mandate(&mut self) -> Result<MandateDefinition, ParseError> {
6805        let tok = self.consume(TokenType::Mandate)?;
6806        let name = self.consume(TokenType::Identifier)?.value;
6807        let mut node = MandateDefinition {
6808            name,
6809            constraint: String::new(),
6810            kp: None,
6811            ki: None,
6812            kd: None,
6813            tolerance: None,
6814            max_steps: None,
6815            drift_bound: None,
6816            lipschitz: None,
6817            on_violation: String::new(),
6818            loc: Loc {
6819                line: tok.line,
6820                column: tok.column,
6821            },
6822            leading_trivia: Vec::new(),
6823            trailing_trivia: Vec::new(),
6824        };
6825        self.consume(TokenType::LBrace)?;
6826        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6827            let field_name = self.current().value.clone();
6828            self.advance();
6829            if self.check(TokenType::Colon) {
6830                self.advance();
6831                match field_name.as_str() {
6832                    "constraint" => {
6833                        node.constraint = self.consume(TokenType::StringLit)?.value.clone()
6834                    }
6835                    "kp" | "Kp" => node.kp = self.parse_optional_float(),
6836                    "ki" | "Ki" => node.ki = self.parse_optional_float(),
6837                    "kd" | "Kd" => node.kd = self.parse_optional_float(),
6838                    "max_steps" => node.max_steps = self.parse_optional_int(),
6839                    // §Fase 119.b — `epsilon:` is what the README publishes; `tolerance:`
6840                    // is what the parser has always accepted. They are the SAME ε — the
6841                    // convergence band of `Converge(e, ε, N)`. Both spellings resolve here
6842                    // rather than one of them silently vanishing into `skip_value()`.
6843                    "tolerance" | "epsilon" => node.tolerance = self.parse_optional_float(),
6844                    "on_violation" => {
6845                        node.on_violation = self.consume_any_ident_or_kw()?.value.clone()
6846                    }
6847                    _ => self.skip_value(),
6848                }
6849            } else if self.check(TokenType::LBrace) {
6850                // §Fase 119.b — `pid { Kp: 2.0, Ki: 0.3, Kd: 0.1 }`, which is the form
6851                // README §XV publishes and the form every mandate example uses.
6852                //
6853                // THIS BLOCK USED TO BE `skip_braced_block()`. The consequence was not a
6854                // parse error — it was SILENT ACCEPTANCE: `axon check` printed
6855                // "0 errors" and the IR came out with `kp: None, ki: None, kd: None`.
6856                // The developer wrote the published example, the compiler agreed, and the
6857                // ENTIRE CONTROL LAW was discarded between them. A dropped specification
6858                // that reports success is the §111 defect living in the parser.
6859                if field_name == "pid" {
6860                    self.parse_pid_block(&mut node)?;
6861                } else if field_name == "stability" {
6862                    self.parse_stability_block(&mut node)?;
6863                } else {
6864                    self.skip_braced_block()?;
6865                }
6866            }
6867        }
6868        self.consume(TokenType::RBrace)?;
6869        Ok(node)
6870    }
6871
6872    /// §Fase 119.b — `pid { Kp: <f>, Ki: <f>, Kd: <f> }`.
6873    ///
6874    /// The gains of the Cybernetic Refinement Calculus controller
6875    /// (`docs/papers/paper_mandate.md` §3): `u(t) = Kp·e(t) + Ki·∫e + Kd·de/dt`.
6876    /// Accepts both capitalised (`Kp`, the papers' and README's notation) and
6877    /// lower-case spellings, because the flat `kp:` form was already accepted and
6878    /// removing it would break programs that use it.
6879    ///
6880    /// §Fase 119.h — unknown keys inside the block are REFUSED.
6881    ///
6882    /// §119.b left them skipped, reasoning that the enclosing declaration behaves
6883    /// that way and tightening it was a wider decision. Measuring the published
6884    /// 2.84.0 binary showed what that costs, and the cost is not symmetric:
6885    /// misspelling a GAIN is caught (the missing gain fails the sign conditions),
6886    /// but misspelling a BOUND is not — `stability { drift: 0.5, L: 0.25 }`
6887    /// compiles clean, and the mandate is admitted with no Lyapunov floor at all.
6888    /// The typo does not weaken the check, it DELETES it.
6889    ///
6890    /// These two blocks are not like the enclosing declaration. They are closed
6891    /// catalogues of three and two keys, every one of which is a proof obligation,
6892    /// and an unrecognised key here is never a field a later version will use —
6893    /// it is a typo whose price is a silently discharged safety property.
6894    fn parse_pid_block(&mut self, node: &mut MandateDefinition) -> Result<(), ParseError> {
6895        self.consume(TokenType::LBrace)?;
6896        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6897            let key_token = self.current().clone();
6898            let key = key_token.value.clone();
6899            self.advance();
6900            if self.check(TokenType::Colon) {
6901                self.advance();
6902                match key.as_str() {
6903                    "kp" | "Kp" => node.kp = self.parse_optional_float(),
6904                    "ki" | "Ki" => node.ki = self.parse_optional_float(),
6905                    "kd" | "Kd" => node.kd = self.parse_optional_float(),
6906                    _ => {
6907                        return Err(ParseError {
6908                            message: format!(
6909                                "`{key}` is not a gain of the PID controller. The block accepts \
6910                                 exactly `Kp`, `Ki` and `Kd` (lower-case spellings too). \
6911                                 Skipping what it does not recognise would let a typo drop a \
6912                                 gain, and the stability band is computed from all three."
6913                            ),
6914                            line: key_token.line,
6915                            column: key_token.column,
6916                            ..Default::default()
6917                        });
6918                    }
6919                }
6920            }
6921            if self.check(TokenType::Comma) {
6922                self.advance();
6923            }
6924        }
6925        self.consume(TokenType::RBrace)?;
6926        Ok(())
6927    }
6928
6929    /// §Fase 119.b — `stability { D: <f>, L: <f> }`.
6930    ///
6931    /// The declared hypotheses of the mandate's stability theorem: `D` is the
6932    /// drift bound `sup|drift(t)|` (paper_mandate §3), `L` the Lipschitz
6933    /// constant of the refinement map (prompt_opt §6.3). With them declared,
6934    /// the type checker verifies the full band `D < |Kp+Ki+Kd| < 1/L`; without
6935    /// them it can verify only the sign conditions, which the papers show to be
6936    /// necessary but not sufficient. The declaration travels in the IR as a
6937    /// proof obligation for dispatch — the compiler never invents these
6938    /// numbers, because they are measured properties of a backend it cannot
6939    /// see, and fabricating them would make the static check vacuous.
6940    ///
6941    /// An empty block is a PARSE error, not a silent no-op: `stability { }`
6942    /// asserts nothing, can discharge nothing, and the developer who wrote it
6943    /// believed otherwise.
6944    fn parse_stability_block(
6945        &mut self,
6946        node: &mut MandateDefinition,
6947    ) -> Result<(), ParseError> {
6948        let open = self.consume(TokenType::LBrace)?;
6949        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6950            let key_token = self.current().clone();
6951            let key = key_token.value.clone();
6952            self.advance();
6953            if self.check(TokenType::Colon) {
6954                self.advance();
6955                match key.as_str() {
6956                    "D" | "d" | "drift_bound" => {
6957                        node.drift_bound = self.parse_optional_float()
6958                    }
6959                    "L" | "l" | "lipschitz" => node.lipschitz = self.parse_optional_float(),
6960                    // §Fase 119.h — see `parse_pid_block`. This is the arm that
6961                    // was actually dangerous: a dropped bound is a dropped
6962                    // hypothesis, and the theorem it guards then holds vacuously.
6963                    _ => {
6964                        return Err(ParseError {
6965                            message: format!(
6966                                "`{key}` is not a hypothesis of the stability theorem. The block \
6967                                 accepts exactly `D` (the drift bound, also spelled `d` or \
6968                                 `drift_bound`) and `L` (the Lipschitz constant, also `l` or \
6969                                 `lipschitz`). This is an error rather than a skipped key \
6970                                 because a bound that fails to parse is a bound that is not \
6971                                 declared, and the compiler would then verify the band it can \
6972                                 see — the sign conditions — and admit the mandate as if the \
6973                                 rest had been checked."
6974                            ),
6975                            line: key_token.line,
6976                            column: key_token.column,
6977                            ..Default::default()
6978                        });
6979                    }
6980                }
6981            }
6982            if self.check(TokenType::Comma) {
6983                self.advance();
6984            }
6985        }
6986        self.consume(TokenType::RBrace)?;
6987        if node.drift_bound.is_none() && node.lipschitz.is_none() {
6988            return Err(ParseError {
6989                message: "the `stability { }` block declares neither `D` nor `L` — it                           asserts nothing and can discharge nothing. Declare the drift                           bound (`D:`), the Lipschitz constant (`L:`), or both; or remove                           the block."
6990                    .to_string(),
6991                line: open.line,
6992                column: open.column,
6993                ..Default::default()
6994            });
6995        }
6996        Ok(())
6997    }
6998
6999    /// §Fase 111.f — `compute <Name>(p: T, …) -> T { <expr> }`.
7000    ///
7001    /// # What this used to be
7002    ///
7003    /// ```text
7004    /// // Skip optional parameters/return type before brace
7005    /// while !self.check(TokenType::LBrace) { self.advance(); }
7006    /// ```
7007    ///
7008    /// The parameters and the return type were **skipped token by token**, and
7009    /// the brace held only `shield:`. So a `compute` had **no inputs, no output
7010    /// type and no body** — which is why the runtime could do nothing but bind
7011    /// the literal string `"compute:Name(args)"`, and why a downstream step then
7012    /// consumed that text where it expected a number. The README meanwhile
7013    /// promised "native Fast-Path execution bypassing the LLM" **with an O(n)
7014    /// guarantee**.
7015    ///
7016    /// # What it is now
7017    ///
7018    /// A named pure function over the §70 expression language — the closed,
7019    /// total, side-effect-free term algebra the runtime already evaluates
7020    /// natively (`eval_expr`, the same evaluator behind `let`, `grad` and
7021    /// `conditional`). Linear in the term, no model in the loop: the advertised
7022    /// claim, made true rather than louder.
7023    ///
7024    /// The legacy field form (`compute N { shield: G }`) still parses — its body
7025    /// is simply `None`, and applying a bodyless compute is refused (axon-T941)
7026    /// instead of silently binding a placeholder.
7027    fn parse_compute(&mut self) -> Result<ComputeDefinition, ParseError> {
7028        let tok = self.consume(TokenType::Compute)?;
7029        let name = self.consume(TokenType::Identifier)?.value;
7030        let mut node = ComputeDefinition {
7031            name,
7032            shield_ref: String::new(),
7033            parameters: Vec::new(),
7034            return_type: String::new(),
7035            body: None,
7036            loc: Loc {
7037                line: tok.line,
7038                column: tok.column,
7039            },
7040            leading_trivia: Vec::new(),
7041            trailing_trivia: Vec::new(),
7042        };
7043
7044        // `(p: T, q: T)` — the typed parameters (they used to be skipped).
7045        if self.check(TokenType::LParen) {
7046            self.advance();
7047            while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
7048                let ptok = self.current().clone();
7049                let pname = self.consume_any_ident_or_kw()?.value.clone();
7050                self.consume(TokenType::Colon)?;
7051                let ptype = self.parse_type_expr()?;
7052                node.parameters.push(Parameter {
7053                    name: pname,
7054                    type_expr: ptype,
7055                    loc: self.loc_of(&ptok),
7056                });
7057                if self.check(TokenType::Comma) {
7058                    self.advance();
7059                }
7060            }
7061            self.consume(TokenType::RParen)?;
7062        }
7063
7064        // `-> T` — the declared result type.
7065        if self.check(TokenType::Arrow) {
7066            self.advance();
7067            node.return_type = self.consume_any_ident_or_kw()?.value.clone();
7068        }
7069
7070        self.consume(TokenType::LBrace)?;
7071        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7072            // A `<name>:` pair is a legacy field (only `shield:` is meaningful).
7073            // Anything else is THE BODY — a §70 expression.
7074            //
7075            // NOTE: the field name may be a KEYWORD, not just an identifier —
7076            // `shield` is `TokenType::Shield`. Testing only for `Identifier` here
7077            // sent `compute N { shield: G }` (the legacy declaration form, and
7078            // the shape of the shipped canonical program) down the
7079            // expression-parsing path and broke it. Back-compat is not optional:
7080            // an adopter's existing program must keep compiling.
7081            let is_field = self
7082                .tokens
7083                .get(self.pos + 1)
7084                .map(|t| t.ttype == TokenType::Colon)
7085                .unwrap_or(false);
7086            if is_field {
7087                let field_name = self.current().value.clone();
7088                self.advance();
7089                self.consume(TokenType::Colon)?;
7090                match field_name.as_str() {
7091                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
7092                    _ => self.skip_value(),
7093                }
7094            } else {
7095                node.body = Some(self.parse_expr()?);
7096            }
7097        }
7098        self.consume(TokenType::RBrace)?;
7099        Ok(node)
7100    }
7101
7102    fn parse_daemon(&mut self) -> Result<DaemonDefinition, ParseError> {
7103        let tok = self.consume(TokenType::Daemon)?;
7104        let name = self.consume(TokenType::Identifier)?.value;
7105        let mut node = DaemonDefinition {
7106            name,
7107            goal: String::new(),
7108            tools: Vec::new(),
7109            memory_ref: String::new(),
7110            strategy: String::new(),
7111            on_stuck: String::new(),
7112            shield_ref: String::new(),
7113            window_ref: String::new(),
7114            budget: None,
7115            max_tokens: None,
7116            max_time: String::new(),
7117            max_cost: None,
7118            listeners: Vec::new(),
7119            requires_capabilities: Vec::new(),
7120            loc: Loc {
7121                line: tok.line,
7122                column: tok.column,
7123            },
7124            leading_trivia: Vec::new(),
7125            trailing_trivia: Vec::new(),
7126        };
7127        // Skip optional parameters/return type before brace
7128        while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
7129            self.advance();
7130        }
7131        self.consume(TokenType::LBrace)?;
7132        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7133            let field = self.current().clone();
7134            let field_name = field.value.clone();
7135            self.advance();
7136            if self.check(TokenType::Colon) {
7137                self.advance();
7138                match field_name.as_str() {
7139                    "goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
7140                    "tools" => node.tools = self.parse_bracketed_identifiers()?,
7141                    "memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
7142                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
7143                    "on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
7144                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
7145                    // §Fase 71.c — `window: <WindowName>` temporal binding.
7146                    "window" => node.window_ref = self.consume_any_ident_or_kw()?.value.clone(),
7147                    "max_tokens" => node.max_tokens = self.parse_optional_int(),
7148                    "max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
7149                    "max_cost" => node.max_cost = self.parse_optional_float(),
7150                    // §Fase 52.d — `requires: [cap, …]` capability scope (same
7151                    // closed slug grammar as `axonendpoint requires:`). The
7152                    // enterprise supervisor mints a per-run principal scoped to
7153                    // exactly these (least privilege).
7154                    "requires" => {
7155                        let bracket_tok = self.current().clone();
7156                        let items = self.parse_bracketed_dot_identifiers()?;
7157                        for slug in &items {
7158                            if !is_valid_capability_slug(slug) {
7159                                return Err(ParseError {
7160                                    message: format!(
7161                                        "Invalid capability slug '{slug}' in daemon '{}' \
7162                                         `requires:`. Capability slugs must match \
7163                                         ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
7164                                         lowercase identifiers. Examples: `daemon.run`, \
7165                                         `memory.write`, `flow.execute`.",
7166                                        node.name
7167                                    ),
7168                                    line: bracket_tok.line,
7169                                    column: bracket_tok.column,
7170                                    ..Default::default()
7171                                });
7172                            }
7173                        }
7174                        node.requires_capabilities = items;
7175                    }
7176                    _ => self.skip_value(),
7177                }
7178            } else if field.ttype == TokenType::Listen {
7179                // §λ-L-E Fase 13 D4 — preserve listen blocks for type
7180                // checking.  We backtracked past the `listen` keyword
7181                // by `advance()` above, so reconstruct a synthetic
7182                // listener using the same dual-mode dispatch the flow
7183                // step parser uses (string topic OR typed channel ref).
7184                let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
7185                    (self.consume(TokenType::StringLit)?.value.clone(), false)
7186                } else {
7187                    (self.consume_any_ident_or_kw()?.value.clone(), true)
7188                };
7189                let mut alias = String::new();
7190                if !self.at_declaration_start()
7191                    && !self.check(TokenType::RBrace)
7192                    && !self.check(TokenType::LBrace)
7193                {
7194                    let next = self.current().clone();
7195                    if next.value == "as" || next.ttype == TokenType::As {
7196                        self.advance();
7197                        alias = self.consume_any_ident_or_kw()?.value.clone();
7198                    }
7199                }
7200                let listen_loc = Loc {
7201                    line: field.line,
7202                    column: field.column,
7203                };
7204                // §Fase 52.a — parse the handler body (was skipped). This is
7205                // what makes a `daemon` operational: the body runs per event /
7206                // scheduled tick (e.g. a `listen "cron:…" as tick { run … }`).
7207                let body = self.parse_listener_body()?;
7208                node.listeners.push(ListenStep {
7209                    channel,
7210                    channel_is_ref,
7211                    event_alias: alias,
7212                    body,
7213                    loc: listen_loc,
7214                });
7215            } else if field_name == "budget" && self.check(TokenType::LBrace) {
7216                // §Fase 72.a — the `budget { … }` linear-effect rate-limit block.
7217                node.budget = Some(self.parse_budget_block(field.line, field.column)?);
7218            } else if self.check(TokenType::LBrace) {
7219                self.skip_braced_block()?;
7220            }
7221        }
7222        self.consume(TokenType::RBrace)?;
7223        Ok(node)
7224    }
7225
7226    /// §Fase 114.a — a TOP-LEVEL `budget <Name> { … }`.
7227    ///
7228    /// Same body as the daemon-attached block; what it gains is a **name** and a
7229    /// **scope that is not a daemon**. Until §114, `budget` was a field of `daemon`
7230    /// and of nothing else — so an adopter deploying an HTTP endpoint that calls a
7231    /// vendor tool had **no way in the language to bound how often it did that.**
7232    /// Not "the bound did not work": **the bound could not be written.** And the
7233    /// HTTP endpoint is what people actually deploy.
7234    fn parse_top_level_budget(&mut self) -> Result<BudgetBlock, ParseError> {
7235        let kw = self.consume(TokenType::Budget)?; // `budget`
7236        let name = self.consume(TokenType::Identifier)?.value;
7237        let mut block = self.parse_budget_block(kw.line, kw.column)?;
7238        block.name = name;
7239        Ok(block)
7240    }
7241
7242    /// §Fase 72.a — `budget { <rate|max>: N per <period> on Tool(<X>) … [on_exhausted: <p>] }`.
7243    fn parse_budget_block(&mut self, line: u32, column: u32) -> Result<BudgetBlock, ParseError> {
7244        self.consume(TokenType::LBrace)?;
7245        let mut quotas = Vec::new();
7246        let mut on_exhausted = String::new();
7247        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7248            let field = self.current().clone();
7249            let field_name = self.consume_any_ident_or_kw()?.value;
7250            match field_name.as_str() {
7251                "rate" | "max" => {
7252                    quotas.push(self.parse_budget_quota(field_name, field.line, field.column)?);
7253                }
7254                "on_exhausted" => {
7255                    self.consume(TokenType::Colon)?;
7256                    on_exhausted = self.consume_any_ident_or_kw()?.value;
7257                }
7258                _ => self.skip_value(),
7259            }
7260        }
7261        self.consume(TokenType::RBrace)?;
7262        Ok(BudgetBlock {
7263            name: String::new(),
7264            quotas,
7265            on_exhausted,
7266            loc: Loc { line, column },
7267            leading_trivia: Vec::new(),
7268            trailing_trivia: Vec::new(),
7269        })
7270    }
7271
7272    /// §Fase 72.a — one quota line: `<kind>: <limit> per <period> on Tool(<effect>)`.
7273    /// `kind` (`rate`/`max`) is already consumed by the caller.
7274    fn parse_budget_quota(
7275        &mut self,
7276        kind: String,
7277        line: u32,
7278        column: u32,
7279    ) -> Result<BudgetQuota, ParseError> {
7280        self.consume(TokenType::Colon)?;
7281        let limit = self.consume_number()? as i64;
7282        // `per <period>`
7283        let _per = self.consume_any_ident_or_kw()?; // the `per` keyword
7284        let period = self.consume_any_ident_or_kw()?.value;
7285        // `on Tool(<effect>)`
7286        let _on = self.consume_any_ident_or_kw()?; // the `on` keyword
7287        let _tool = self.consume_any_ident_or_kw()?; // the `Tool` wrapper keyword
7288        self.consume(TokenType::LParen)?;
7289        let effect = self.consume_any_ident_or_kw()?.value;
7290        self.consume(TokenType::RParen)?;
7291        Ok(BudgetQuota {
7292            kind,
7293            limit,
7294            period,
7295            effect,
7296            loc: Loc { line, column },
7297        })
7298    }
7299
7300    fn parse_axonstore(&mut self) -> Result<AxonStoreDefinition, ParseError> {
7301        let tok = self.consume(TokenType::AxonStore)?;
7302        let name = self.consume(TokenType::Identifier)?.value;
7303        let mut node = AxonStoreDefinition {
7304            name,
7305            backend: String::new(),
7306            connection: String::new(),
7307            resource_ref: String::new(),
7308            confidence_floor: None,
7309            isolation: String::new(),
7310            on_breach: String::new(),
7311            capability: String::new(),
7312            class: String::new(),
7313            column_schema: None,
7314            loc: Loc {
7315                line: tok.line,
7316                column: tok.column,
7317            },
7318            leading_trivia: Vec::new(),
7319            trailing_trivia: Vec::new(),
7320        };
7321        self.consume(TokenType::LBrace)?;
7322        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7323            let field = self.current().clone();
7324            let field_name = field.value.clone();
7325            // §Fase 38.b (D1) — `schema:` declaration in three closed
7326            // forms: inline column block, manifest reference (string
7327            // literal), or env-var schema namespace (`env:VAR` —
7328            // unquoted or quoted). Parse the form; the §38.d / §38.e
7329            // type-checker consumes the resulting AST.
7330            if field.ttype == TokenType::Schema {
7331                self.advance();
7332                let parsed = self.parse_store_schema_declaration(&node.name, field.line, field.column)?;
7333                node.column_schema = Some(parsed);
7334                continue;
7335            }
7336            self.advance();
7337            if self.check(TokenType::Colon) {
7338                self.advance();
7339                match field_name.as_str() {
7340                    "backend" => node.backend = self.consume_any_ident_or_kw()?.value.clone(),
7341                    // §Fase 94.a — the secret-class prefix of a
7342                    // `backend: secrets` metadata store. Dotted-identifier
7343                    // form (`class: crm`, `class: crm.oauth`); the
7344                    // secrets-only placement rule + slug shape are
7345                    // `axon-T900` in the type-checker (it needs the
7346                    // resolved `backend:`, which may appear after this
7347                    // field in source order).
7348                    "class" => node.class = self.parse_dotted_identifier()?,
7349                    "connection" => node.connection = self.parse_config_key()?,
7350                    // §Fase 113 — the `resource` this store RUNS ON. When
7351                    // present the store derives its DSN, its POOL SIZE and its
7352                    // sharing discipline from the resource; `connection:`
7353                    // becomes redundant and `axon-T946` refuses declaring both
7354                    // (the same fact, twice, is how the islands happened).
7355                    "resource" => {
7356                        node.resource_ref = self.consume_any_ident_or_kw()?.value.clone()
7357                    }
7358                    "confidence_floor" => node.confidence_floor = self.parse_optional_float(),
7359                    "isolation" => node.isolation = self.consume_any_ident_or_kw()?.value.clone(),
7360                    "on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
7361                    // §Fase 35.j (D11) — Pillar IV: the capability slug
7362                    // required to access this store. Validated against
7363                    // the closed slug grammar shared with `requires:`.
7364                    "capability" => {
7365                        let slug_tok = self.consume(TokenType::StringLit)?.clone();
7366                        if !is_valid_capability_slug(&slug_tok.value) {
7367                            return Err(ParseError {
7368                                message: format!(
7369                                    "Invalid capability slug '{}' in axonstore '{}' \
7370                                     `capability:`. Capability slugs must match \
7371                                     ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
7372                                     lowercase identifiers starting with a letter. Examples: \
7373                                     `admin`, `tenant.read`, `hipaa.phi.read`.",
7374                                    slug_tok.value, node.name
7375                                ),
7376                                line: slug_tok.line,
7377                                column: slug_tok.column,
7378                                ..Default::default()
7379                            });
7380                        }
7381                        node.capability = slug_tok.value.clone();
7382                    }
7383                    _ => self.skip_value(),
7384                }
7385            } else if self.check(TokenType::LBrace) {
7386                self.skip_braced_block()?;
7387            }
7388        }
7389        self.consume(TokenType::RBrace)?;
7390        Ok(node)
7391    }
7392
7393    /// §Fase 38.b (D1) — parse the three closed forms of an `axonstore`
7394    /// `schema:` declaration:
7395    ///
7396    ///   * form (a) **inline** — `schema { col: Type [constraint…], … }`
7397    ///   * form (b) **manifest reference** — `schema: "qualified.name"`
7398    ///     (string literal that does NOT start with `env:`)
7399    ///   * form (c) **env-var schema namespace** — `schema: env:VAR`
7400    ///     (unquoted) OR `schema: "env:VAR"` (quoted; the literal
7401    ///     starts with `env:`)
7402    ///
7403    /// Called immediately AFTER `schema` is consumed.
7404    fn parse_store_schema_declaration(
7405        &mut self,
7406        store_name: &str,
7407        sch_line: u32,
7408        sch_col: u32,
7409    ) -> Result<crate::store_schema::StoreColumnSchema, ParseError> {
7410        use crate::store_schema::{StoreColumn, StoreColumnSchema, StoreColumnType};
7411
7412        // — Form (a) — inline column block: `schema { ... }`. —
7413        if self.check(TokenType::LBrace) {
7414            self.consume(TokenType::LBrace)?;
7415            let mut columns: Vec<StoreColumn> = Vec::new();
7416            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7417                let col_tok = self.current().clone();
7418                let col_name = self.consume_any_ident_or_kw()?.value.clone();
7419                self.consume(TokenType::Colon)?;
7420                let type_tok = self.consume_any_ident_or_kw()?.clone();
7421                let col_type = StoreColumnType::from_token(&type_tok.value).ok_or_else(|| {
7422                    let names = StoreColumnType::all_canonical_names();
7423                    let suggestion =
7424                        crate::smart_suggest::suggest_for(&type_tok.value, &names);
7425                    let suggest_suffix = if suggestion.is_empty() {
7426                        String::new()
7427                    } else {
7428                        format!(" {suggestion}")
7429                    };
7430                    let known = names.join(", ");
7431                    ParseError {
7432                        message: format!(
7433                            "Unknown column type `{}` for column `{}` in \
7434                             axonstore `{}` `schema:` block. The closed \
7435                             v1.38.0 column-type catalog (Fase 38.b D1) \
7436                             is {{{known}}} (plus common lowercase \
7437                             aliases — `int`/`integer`/`int4` for \
7438                             `Int`, `bool`/`boolean` for `Bool`, etc.).\
7439                             {suggest_suffix}",
7440                            type_tok.value, col_name, store_name
7441                        ),
7442                        line: type_tok.line,
7443                        column: type_tok.column,
7444                        ..Default::default()
7445                    }
7446                })?;
7447
7448                // §Fase 73.a (D1) — the OPTIONAL `Json<T>` shape LENS on a
7449                // column. `payload: Json<UserEvent>` records the expected
7450                // struct shape; the lens is a compile-time expectation only
7451                // (the column stays physically `jsonb`, navigated totally at
7452                // runtime — doctrine `open_data_is_total`). The shape's
7453                // well-formedness (T is a declared `type`) is `axon-T840`
7454                // in the type-checker — it needs the symbol table. Here we
7455                // only enforce the STRUCTURAL rule: a `<T>` lens may refine
7456                // ONLY a `Json` / `Jsonb` column — `axon-T841` otherwise.
7457                let mut json_shape: Option<String> = None;
7458                if self.check(TokenType::Lt) {
7459                    self.advance();
7460                    let shape_tok = self.consume_any_ident_or_kw()?.clone();
7461                    self.consume(TokenType::Gt)?;
7462                    if matches!(col_type, StoreColumnType::Json | StoreColumnType::Jsonb) {
7463                        json_shape = Some(shape_tok.value.clone());
7464                    } else {
7465                        return Err(ParseError {
7466                            message: format!(
7467                                "axon-T841 a shape lens `<{shape}>` may refine \
7468                                 only a `Json` / `Jsonb` column, but column \
7469                                 `{col}` in axonstore `{store}` is `{ty}`. Drop \
7470                                 the `<{shape}>` (a rigid column already has a \
7471                                 fixed shape), or change the column type to \
7472                                 `Json<{shape}>` if it carries open documents.",
7473                                shape = shape_tok.value,
7474                                col = col_name,
7475                                store = store_name,
7476                                ty = col_type.canonical_name(),
7477                            ),
7478                            line: shape_tok.line,
7479                            column: shape_tok.column,
7480                            ..Default::default()
7481                        });
7482                    }
7483                }
7484
7485                let mut col = StoreColumn {
7486                    name: col_name,
7487                    col_type,
7488                    json_shape,
7489                    primary_key: false,
7490                    auto_increment: false,
7491                    not_null: false,
7492                    unique: false,
7493                    indexed: false,
7494                    default_value: String::new(),
7495                    // §Fase 38.x.d (D1) — `identity` is now a recognized
7496                    // inline keyword (see the constraint loop below).
7497                    // Defaults to false; set to true when the adopter
7498                    // writes `id: BigInt primary_key identity`.
7499                    identity: false,
7500                    line: col_tok.line,
7501                    column: col_tok.column,
7502                };
7503
7504                // Trailing constraints (position-independent), matching
7505                // the Python `_parse_store_column` surface.
7506                while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7507                    if self.current().ttype != TokenType::Identifier {
7508                        // The next column starts with a non-identifier
7509                        // (rare) — stop the constraint scan.
7510                        break;
7511                    }
7512                    let constraint = self.current().value.clone();
7513                    match constraint.as_str() {
7514                        "primary_key" => {
7515                            col.primary_key = true;
7516                            self.advance();
7517                        }
7518                        "auto_increment" => {
7519                            col.auto_increment = true;
7520                            self.advance();
7521                        }
7522                        "not_null" => {
7523                            col.not_null = true;
7524                            self.advance();
7525                        }
7526                        "unique" => {
7527                            col.unique = true;
7528                            self.advance();
7529                        }
7530                        // §Fase 73.f (D1) — the `index` constraint declares
7531                        // an index as a capability-honest effect (visible to
7532                        // the deploy gate, not a silent DBA action). The
7533                        // backend picks the method from the column type
7534                        // (GIN for a Json/Jsonb column, b-tree otherwise).
7535                        "index" => {
7536                            col.indexed = true;
7537                            self.advance();
7538                        }
7539                        // §Fase 38.x.d (D1) — `identity` marks a column
7540                        // as `GENERATED ALWAYS/BY DEFAULT AS IDENTITY`.
7541                        // Distinct from `auto_increment` (legacy SERIAL
7542                        // via `nextval(...)` default). T803 skips
7543                        // identity columns from the NOT-NULL-omission
7544                        // check because Postgres auto-fills them; the
7545                        // distinction matters because IDENTITY ALWAYS
7546                        // also rejects user-supplied values, where
7547                        // SERIAL accepts them (a future 38.x.e arm in
7548                        // T802 may surface this).
7549                        "identity" => {
7550                            col.identity = true;
7551                            self.advance();
7552                        }
7553                        "default" => {
7554                            self.advance();
7555                            let dv = self.current().clone();
7556                            if matches!(
7557                                dv.ttype,
7558                                TokenType::StringLit
7559                                    | TokenType::Integer
7560                                    | TokenType::Float
7561                            ) {
7562                                col.default_value = dv.value.clone();
7563                                self.advance();
7564                            } else {
7565                                col.default_value =
7566                                    self.consume_any_ident_or_kw()?.value.clone();
7567                            }
7568                        }
7569                        _ => break,
7570                    }
7571                }
7572
7573                columns.push(col);
7574            }
7575            self.consume(TokenType::RBrace)?;
7576            return Ok(StoreColumnSchema::Inline {
7577                columns,
7578                leading_trivia: Vec::new(),
7579                line: sch_line,
7580                column: sch_col,
7581            });
7582        }
7583
7584        // — Forms (b) + (c) require a `:` separator. —
7585        if !self.check(TokenType::Colon) {
7586            let cur = self.current().clone();
7587            return Err(ParseError {
7588                message: format!(
7589                    "axonstore `{store_name}` `schema:` declaration expects \
7590                     `{{ … }}` (inline columns), `: \"manifest.ref\"` \
7591                     (manifest reference), or `: env:VAR` (per-tenant schema \
7592                     namespace). Got `{}` instead.",
7593                    cur.value
7594                ),
7595                line: cur.line,
7596                column: cur.column,
7597                ..Default::default()
7598            });
7599        }
7600        self.consume(TokenType::Colon)?;
7601
7602        // — Form (b) or (c)-quoted — string literal value. —
7603        if self.check(TokenType::StringLit) {
7604            let lit = self.consume(TokenType::StringLit)?.clone();
7605            let value = lit.value.clone();
7606            if let Some(var) = value.strip_prefix("env:") {
7607                let var = var.trim();
7608                if var.is_empty() {
7609                    return Err(ParseError {
7610                        message: format!(
7611                            "axonstore `{store_name}` `schema: \"env:\"` is \
7612                             missing the variable name after the `env:` \
7613                             prefix."
7614                        ),
7615                        line: lit.line,
7616                        column: lit.column,
7617                        ..Default::default()
7618                    });
7619                }
7620                return Ok(StoreColumnSchema::EnvVar {
7621                    var_name: var.to_string(),
7622                    line: sch_line,
7623                    column: sch_col,
7624                });
7625            }
7626            // Plain string → manifest reference.
7627            if value.trim().is_empty() {
7628                return Err(ParseError {
7629                    message: format!(
7630                        "axonstore `{store_name}` `schema:` manifest reference \
7631                         is empty. Expected `\"qualified.name\"` — e.g. \
7632                         `\"public.tenants\"`."
7633                    ),
7634                    line: lit.line,
7635                    column: lit.column,
7636                    ..Default::default()
7637                });
7638            }
7639            return Ok(StoreColumnSchema::ManifestRef {
7640                qualified_name: value,
7641                line: sch_line,
7642                column: sch_col,
7643            });
7644        }
7645
7646        // — Form (c) unquoted — `env:VAR`. The lexer emits `env` as an
7647        //   identifier, then `:`, then the identifier var name. —
7648        let env_tok = self.current().clone();
7649        if env_tok.value == "env" {
7650            self.advance();
7651            if !self.check(TokenType::Colon) {
7652                return Err(ParseError {
7653                    message: format!(
7654                        "axonstore `{store_name}` `schema: env` is missing the \
7655                         `:` separator. Expected `schema: env:VAR`."
7656                    ),
7657                    line: env_tok.line,
7658                    column: env_tok.column,
7659                    ..Default::default()
7660                });
7661            }
7662            self.advance(); // past ':'
7663            let var_tok = self.consume_any_ident_or_kw()?.clone();
7664            if var_tok.value.trim().is_empty() {
7665                return Err(ParseError {
7666                    message: format!(
7667                        "axonstore `{store_name}` `schema: env:` is missing \
7668                         the variable name."
7669                    ),
7670                    line: var_tok.line,
7671                    column: var_tok.column,
7672                    ..Default::default()
7673                });
7674            }
7675            return Ok(StoreColumnSchema::EnvVar {
7676                var_name: var_tok.value.clone(),
7677                line: sch_line,
7678                column: sch_col,
7679            });
7680        }
7681
7682        Err(ParseError {
7683            message: format!(
7684                "axonstore `{store_name}` `schema:` declaration expects \
7685                 `{{ … }}` (inline columns), `\"manifest.ref\"` (manifest \
7686                 reference), or `env:VAR` (per-tenant schema namespace). \
7687                 Got `{}` instead.",
7688                env_tok.value
7689            ),
7690            line: env_tok.line,
7691            column: env_tok.column,
7692            ..Default::default()
7693        })
7694    }
7695
7696    // ── §λ-L-E Fase 1 — Resource primitive ────────────────────────
7697
7698    /// Parse: `resource Name { kind, endpoint, capacity, lifetime, certainty_floor, shield }`.
7699    ///
7700    /// Mirrors `axon.compiler.parser.Parser._parse_resource`. Unknown fields
7701    /// are silently skipped (keeps the grammar forward-compatible).
7702    fn parse_resource(&mut self) -> Result<ResourceDefinition, ParseError> {
7703        let tok = self.consume(TokenType::Resource)?;
7704        let name = self.consume(TokenType::Identifier)?.value;
7705        let mut node = ResourceDefinition {
7706            name,
7707            kind: String::new(),
7708            endpoint: String::new(),
7709            capacity: None,
7710            lifetime: "affine".to_string(),
7711            certainty_floor: None,
7712            shield_ref: String::new(),
7713            within: String::new(),
7714            loc: Loc {
7715                line: tok.line,
7716                column: tok.column,
7717            },
7718            leading_trivia: Vec::new(),
7719            trailing_trivia: Vec::new(),
7720        };
7721        self.consume(TokenType::LBrace)?;
7722        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7723            let field_tok = self.current().clone();
7724            let field_name = field_tok.value.clone();
7725            self.advance();
7726            if !self.check(TokenType::Colon) {
7727                // Tolerate stray brace or unknown layout.
7728                if self.check(TokenType::LBrace) {
7729                    self.skip_braced_block()?;
7730                }
7731                continue;
7732            }
7733            self.advance(); // past ':'
7734            match field_name.as_str() {
7735                "kind" => node.kind = self.consume_any_ident_or_kw()?.value,
7736                // §Fase 113 — `endpoint:` accepts BOTH shapes on purpose:
7737                //   - a dotted config key  (`endpoint: db.main`)      — the law
7738                //   - a string literal     (`endpoint: "postgres://…"`) — the sin
7739                //
7740                // The literal is REFUSED, but by `axon-T944`, not by the parser.
7741                // If it died here the adopter would read "Expected StringLit",
7742                // which explains nothing. The law gets to say why: *URLs and
7743                // credentials never appear in source* — the same sentence
7744                // `axon-T850` has been saying to `upstream.resolve` all along.
7745                //
7746                // A diagnostic that names the rule teaches; one that names the
7747                // token type only tells you the compiler is unhappy.
7748                "endpoint" => {
7749                    node.endpoint = if self.check(TokenType::StringLit) {
7750                        self.consume(TokenType::StringLit)?.value
7751                    } else {
7752                        self.parse_dotted_identifier()?
7753                    };
7754                }
7755                "capacity" => {
7756                    node.capacity = self.parse_optional_int();
7757                }
7758                "lifetime" => {
7759                    let lt_tok = self.consume_any_ident_or_kw()?;
7760                    let lt = lt_tok.value;
7761                    if !matches!(lt.as_str(), "linear" | "affine" | "persistent") {
7762                        return Err(ParseError {
7763                            message: format!(
7764                                "Invalid lifetime '{lt}' in resource '{}' — \
7765                                 expected linear | affine | persistent",
7766                                node.name
7767                            ),
7768                            line: lt_tok.line,
7769                            column: lt_tok.column,
7770                                                    ..Default::default()
7771                        });
7772                    }
7773                    node.lifetime = lt;
7774                }
7775                "certainty_floor" => {
7776                    node.certainty_floor = self.parse_optional_float();
7777                }
7778                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
7779                // §Fase 113 — `within: <fabric>`. ONE field, so a resource
7780                // cannot be in two fabrics: Separation-Logic disjointness is
7781                // unrepresentable rather than verified.
7782                "within" => node.within = self.consume_any_ident_or_kw()?.value,
7783                // §Fase 113 — an unknown field is a HARD ERROR, not a shrug.
7784                //
7785                // This arm used to be `_ => self.skip_value()`. That is the same
7786                // family as §111's root cause (`parse_block_step` →
7787                // `skip_braced_block()`, which silently killed four primitives):
7788                // a misspelled `withn:` would have been swallowed without a
7789                // word, and the resource would have governed nothing while
7790                // looking governed. A field the parser does not know is a field
7791                // the adopter believes in and the compiler does not.
7792                unknown => {
7793                    return Err(ParseError {
7794                        message: format!(
7795                            "Unknown field '{unknown}' in resource '{}' — expected one of: \
7796                             kind, endpoint, capacity, lifetime, certainty_floor, shield, within",
7797                            node.name
7798                        ),
7799                        line: field_tok.line,
7800                        column: field_tok.column,
7801                        ..Default::default()
7802                    });
7803                }
7804            }
7805        }
7806        self.consume(TokenType::RBrace)?;
7807        Ok(node)
7808    }
7809
7810    /// Parse: `fabric Name { provider, region, zones, ephemeral, shield }`.
7811    fn parse_fabric(&mut self) -> Result<FabricDefinition, ParseError> {
7812        let tok = self.consume(TokenType::Fabric)?;
7813        let name = self.consume(TokenType::Identifier)?.value;
7814        let mut node = FabricDefinition {
7815            name,
7816            provider: String::new(),
7817            region: String::new(),
7818            zones: None,
7819            ephemeral: None,
7820            shield_ref: String::new(),
7821            loc: Loc {
7822                line: tok.line,
7823                column: tok.column,
7824            },
7825            leading_trivia: Vec::new(),
7826            trailing_trivia: Vec::new(),
7827        };
7828        self.consume(TokenType::LBrace)?;
7829        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7830            let field_name = self.current().value.clone();
7831            self.advance();
7832            if !self.check(TokenType::Colon) {
7833                if self.check(TokenType::LBrace) {
7834                    self.skip_braced_block()?;
7835                }
7836                continue;
7837            }
7838            self.advance(); // past ':'
7839            match field_name.as_str() {
7840                "provider" => node.provider = self.consume_any_ident_or_kw()?.value,
7841                "region" => node.region = self.consume(TokenType::StringLit)?.value,
7842                "zones" => node.zones = self.parse_optional_int(),
7843                "ephemeral" => {
7844                    let b = self.parse_bool()?;
7845                    node.ephemeral = Some(b);
7846                }
7847                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
7848                _ => self.skip_value(),
7849            }
7850        }
7851        self.consume(TokenType::RBrace)?;
7852        Ok(node)
7853    }
7854
7855    /// Parse: `manifest Name { resources, fabric, region, zones, compliance }`.
7856    fn parse_manifest(&mut self) -> Result<ManifestDefinition, ParseError> {
7857        let tok = self.consume(TokenType::Manifest)?;
7858        let name = self.consume(TokenType::Identifier)?.value;
7859        let mut node = ManifestDefinition {
7860            name,
7861            resources: Vec::new(),
7862            fabric_ref: String::new(),
7863            region: String::new(),
7864            zones: None,
7865            compliance: Vec::new(),
7866            loc: Loc {
7867                line: tok.line,
7868                column: tok.column,
7869            },
7870            leading_trivia: Vec::new(),
7871            trailing_trivia: Vec::new(),
7872        };
7873        self.consume(TokenType::LBrace)?;
7874        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7875            let field_name = self.current().value.clone();
7876            self.advance();
7877            if !self.check(TokenType::Colon) {
7878                if self.check(TokenType::LBrace) {
7879                    self.skip_braced_block()?;
7880                }
7881                continue;
7882            }
7883            self.advance();
7884            match field_name.as_str() {
7885                "resources" => node.resources = self.parse_bracketed_identifiers()?,
7886                "fabric" => node.fabric_ref = self.consume_any_ident_or_kw()?.value,
7887                "region" => node.region = self.consume(TokenType::StringLit)?.value,
7888                "zones" => node.zones = self.parse_optional_int(),
7889                "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
7890                _ => self.skip_value(),
7891            }
7892        }
7893        self.consume(TokenType::RBrace)?;
7894        Ok(node)
7895    }
7896
7897    /// Parse: `observe Name from Manifest { sources, quorum, timeout, on_partition, certainty_floor }`.
7898    fn parse_observe(&mut self) -> Result<ObserveDefinition, ParseError> {
7899        let tok = self.consume(TokenType::Observe)?;
7900        let name = self.consume(TokenType::Identifier)?.value;
7901        // `from <Manifest>` — required per Python grammar.
7902        self.consume(TokenType::From)?;
7903        let target = self.consume(TokenType::Identifier)?.value;
7904        let mut node = ObserveDefinition {
7905            name,
7906            target,
7907            sources: Vec::new(),
7908            quorum: None,
7909            timeout: String::new(),
7910            on_partition: "fail".to_string(),
7911            certainty_floor: None,
7912            loc: Loc {
7913                line: tok.line,
7914                column: tok.column,
7915            },
7916            leading_trivia: Vec::new(),
7917            trailing_trivia: Vec::new(),
7918        };
7919        self.consume(TokenType::LBrace)?;
7920        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7921            let field_name = self.current().value.clone();
7922            self.advance();
7923            if !self.check(TokenType::Colon) {
7924                if self.check(TokenType::LBrace) {
7925                    self.skip_braced_block()?;
7926                }
7927                continue;
7928            }
7929            self.advance();
7930            match field_name.as_str() {
7931                "sources" => node.sources = self.parse_bracketed_identifiers()?,
7932                "quorum" => node.quorum = self.parse_optional_int(),
7933                "timeout" => {
7934                    let t = self.current().clone();
7935                    match t.ttype {
7936                        TokenType::Duration | TokenType::StringLit => {
7937                            self.advance();
7938                            node.timeout = t.value;
7939                        }
7940                        _ => node.timeout = self.consume_any_ident_or_kw()?.value,
7941                    }
7942                }
7943                "on_partition" => {
7944                    let p_tok = self.consume_any_ident_or_kw()?;
7945                    let p = p_tok.value;
7946                    if !matches!(p.as_str(), "fail" | "shield_quarantine") {
7947                        return Err(ParseError {
7948                            message: format!(
7949                                "Invalid on_partition '{p}' in observe '{}' — \
7950                                 expected fail | shield_quarantine",
7951                                node.name
7952                            ),
7953                            line: p_tok.line,
7954                            column: p_tok.column,
7955                                                    ..Default::default()
7956                        });
7957                    }
7958                    node.on_partition = p;
7959                }
7960                "certainty_floor" => node.certainty_floor = self.parse_optional_float(),
7961                _ => self.skip_value(),
7962            }
7963        }
7964        self.consume(TokenType::RBrace)?;
7965        Ok(node)
7966    }
7967
7968    // ── §λ-L-E Fase 3 — Control cognitivo ─────────────────────────
7969
7970    /// Parse: `reconcile Name { observe, threshold, tolerance, on_drift, shield, mandate, max_retries }`.
7971    fn parse_reconcile(&mut self) -> Result<ReconcileDefinition, ParseError> {
7972        let tok = self.consume(TokenType::Reconcile)?;
7973        let name = self.consume(TokenType::Identifier)?.value;
7974        let mut node = ReconcileDefinition {
7975            name,
7976            observe_ref: String::new(),
7977            threshold: None,
7978            tolerance: None,
7979            on_drift: "provision".to_string(),
7980            shield_ref: String::new(),
7981            mandate_ref: String::new(),
7982            max_retries: 3,
7983            loc: Loc {
7984                line: tok.line,
7985                column: tok.column,
7986            },
7987            leading_trivia: Vec::new(),
7988            trailing_trivia: Vec::new(),
7989        };
7990        self.consume(TokenType::LBrace)?;
7991        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7992            let field_name = self.current().value.clone();
7993            self.advance();
7994            if !self.check(TokenType::Colon) {
7995                if self.check(TokenType::LBrace) {
7996                    self.skip_braced_block()?;
7997                }
7998                continue;
7999            }
8000            self.advance();
8001            match field_name.as_str() {
8002                "observe" => node.observe_ref = self.consume_any_ident_or_kw()?.value,
8003                "threshold" => node.threshold = self.parse_optional_float(),
8004                "tolerance" => node.tolerance = self.parse_optional_float(),
8005                "on_drift" => {
8006                    let d_tok = self.consume_any_ident_or_kw()?;
8007                    let d = d_tok.value;
8008                    if !matches!(d.as_str(), "provision" | "alert" | "refine") {
8009                        return Err(ParseError {
8010                            message: format!(
8011                                "Invalid on_drift '{d}' in reconcile '{}' — \
8012                                 expected provision | alert | refine",
8013                                node.name
8014                            ),
8015                            line: d_tok.line,
8016                            column: d_tok.column,
8017                                                    ..Default::default()
8018                        });
8019                    }
8020                    node.on_drift = d;
8021                }
8022                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
8023                "mandate" => node.mandate_ref = self.consume_any_ident_or_kw()?.value,
8024                "max_retries" => {
8025                    if let Some(v) = self.parse_optional_int() {
8026                        node.max_retries = v;
8027                    }
8028                }
8029                _ => self.skip_value(),
8030            }
8031        }
8032        self.consume(TokenType::RBrace)?;
8033        Ok(node)
8034    }
8035
8036    /// Parse: `lease Name { resource, duration, acquire, on_expire }`.
8037    fn parse_lease(&mut self) -> Result<LeaseDefinition, ParseError> {
8038        let tok = self.consume(TokenType::Lease)?;
8039        let name = self.consume(TokenType::Identifier)?.value;
8040        let mut node = LeaseDefinition {
8041            name,
8042            resource_ref: String::new(),
8043            duration: String::new(),
8044            acquire: "on_start".to_string(),
8045            on_expire: "anchor_breach".to_string(),
8046            loc: Loc {
8047                line: tok.line,
8048                column: tok.column,
8049            },
8050            leading_trivia: Vec::new(),
8051            trailing_trivia: Vec::new(),
8052        };
8053        self.consume(TokenType::LBrace)?;
8054        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8055            let field_name = self.current().value.clone();
8056            self.advance();
8057            if !self.check(TokenType::Colon) {
8058                if self.check(TokenType::LBrace) {
8059                    self.skip_braced_block()?;
8060                }
8061                continue;
8062            }
8063            self.advance();
8064            match field_name.as_str() {
8065                "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
8066                "duration" => {
8067                    let t = self.current().clone();
8068                    match t.ttype {
8069                        TokenType::Duration | TokenType::StringLit => {
8070                            self.advance();
8071                            node.duration = t.value;
8072                        }
8073                        _ => node.duration = self.consume_any_ident_or_kw()?.value,
8074                    }
8075                }
8076                "acquire" => {
8077                    let a_tok = self.consume_any_ident_or_kw()?;
8078                    let a = a_tok.value;
8079                    if !matches!(a.as_str(), "on_start" | "on_demand") {
8080                        return Err(ParseError {
8081                            message: format!(
8082                                "Invalid acquire '{a}' in lease '{}' — \
8083                                 expected on_start | on_demand",
8084                                node.name
8085                            ),
8086                            line: a_tok.line,
8087                            column: a_tok.column,
8088                                                    ..Default::default()
8089                        });
8090                    }
8091                    node.acquire = a;
8092                }
8093                "on_expire" => {
8094                    let e_tok = self.consume_any_ident_or_kw()?;
8095                    let e = e_tok.value;
8096                    if !matches!(e.as_str(), "anchor_breach" | "release" | "extend") {
8097                        return Err(ParseError {
8098                            message: format!(
8099                                "Invalid on_expire '{e}' in lease '{}' — \
8100                                 expected anchor_breach | release | extend",
8101                                node.name
8102                            ),
8103                            line: e_tok.line,
8104                            column: e_tok.column,
8105                                                    ..Default::default()
8106                        });
8107                    }
8108                    node.on_expire = e;
8109                }
8110                _ => self.skip_value(),
8111            }
8112        }
8113        self.consume(TokenType::RBrace)?;
8114        Ok(node)
8115    }
8116
8117    /// Parse: `ensemble Name { observations, quorum, aggregation, certainty_mode }`.
8118    fn parse_ensemble(&mut self) -> Result<EnsembleDefinition, ParseError> {
8119        let tok = self.consume(TokenType::Ensemble)?;
8120        let name = self.consume(TokenType::Identifier)?.value;
8121        let mut node = EnsembleDefinition {
8122            name,
8123            observations: Vec::new(),
8124            quorum: None,
8125            aggregation: "majority".to_string(),
8126            certainty_mode: "min".to_string(),
8127            loc: Loc {
8128                line: tok.line,
8129                column: tok.column,
8130            },
8131            leading_trivia: Vec::new(),
8132            trailing_trivia: Vec::new(),
8133        };
8134        self.consume(TokenType::LBrace)?;
8135        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8136            let field_name = self.current().value.clone();
8137            self.advance();
8138            if !self.check(TokenType::Colon) {
8139                if self.check(TokenType::LBrace) {
8140                    self.skip_braced_block()?;
8141                }
8142                continue;
8143            }
8144            self.advance();
8145            match field_name.as_str() {
8146                "observations" => node.observations = self.parse_bracketed_identifiers()?,
8147                "quorum" => node.quorum = self.parse_optional_int(),
8148                "aggregation" => {
8149                    let a_tok = self.consume_any_ident_or_kw()?;
8150                    let a = a_tok.value;
8151                    if !matches!(a.as_str(), "majority" | "weighted" | "byzantine") {
8152                        return Err(ParseError {
8153                            message: format!(
8154                                "Invalid aggregation '{a}' in ensemble '{}' — \
8155                                 expected majority | weighted | byzantine",
8156                                node.name
8157                            ),
8158                            line: a_tok.line,
8159                            column: a_tok.column,
8160                                                    ..Default::default()
8161                        });
8162                    }
8163                    node.aggregation = a;
8164                }
8165                "certainty_mode" => {
8166                    let c_tok = self.consume_any_ident_or_kw()?;
8167                    let c = c_tok.value;
8168                    if !matches!(c.as_str(), "min" | "weighted" | "harmonic") {
8169                        return Err(ParseError {
8170                            message: format!(
8171                                "Invalid certainty_mode '{c}' in ensemble '{}' — \
8172                                 expected min | weighted | harmonic",
8173                                node.name
8174                            ),
8175                            line: c_tok.line,
8176                            column: c_tok.column,
8177                                                    ..Default::default()
8178                        });
8179                    }
8180                    node.certainty_mode = c;
8181                }
8182                _ => self.skip_value(),
8183            }
8184        }
8185        self.consume(TokenType::RBrace)?;
8186        Ok(node)
8187    }
8188
8189    // ── §λ-L-E Fase 4 — Topology + π-calculus binary sessions ─────
8190
8191    /// Parse: `session Name { role1: [step, …]  role2: [step, …] }`.
8192    ///
8193    /// The enclosing `parse_session_definition` disambiguates from the session
8194    /// step token `session` (which does not exist) by always entering from the
8195    /// top-level dispatch; the identifier role name is consumed after `{`.
8196    fn parse_session_definition(&mut self) -> Result<SessionDefinition, ParseError> {
8197        let tok = self.consume(TokenType::Session)?;
8198        let name = self.consume(TokenType::Identifier)?.value;
8199        let mut node = SessionDefinition {
8200            name,
8201            roles: Vec::new(),
8202            loc: Loc {
8203                line: tok.line,
8204                column: tok.column,
8205            },
8206            leading_trivia: Vec::new(),
8207            trailing_trivia: Vec::new(),
8208        };
8209        self.consume(TokenType::LBrace)?;
8210        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8211            let role_tok = self.consume_any_ident_or_kw()?;
8212            self.consume(TokenType::Colon)?;
8213            let steps = self.parse_session_steps()?;
8214            node.roles.push(SessionRole {
8215                name: role_tok.value,
8216                steps,
8217                loc: Loc {
8218                    line: role_tok.line,
8219                    column: role_tok.column,
8220                },
8221            });
8222        }
8223        self.consume(TokenType::RBrace)?;
8224        Ok(node)
8225    }
8226
8227    /// §Fase 51.c.2 — Parse a Pauli-sum observable declaration:
8228    /// ```text
8229    /// observable EnergyHamiltonian {
8230    ///     qubits: 2
8231    ///     term: 0.5 * "ZZ"
8232    ///     term: -1.2 * "XI"
8233    /// }
8234    /// ```
8235    /// `term:` is a repeatable key (one `cₖ · Pₖ` per line). The coefficient is
8236    /// a real scalar (optional leading `+`/`-`), then `*`, then a quoted Pauli
8237    /// string. The type-checker (§51.c.2) validates the closed `{I,X,Y,Z}`
8238    /// alphabet + equal lengths; real coefficients ⇒ Hermitian by construction.
8239    fn parse_observable(&mut self) -> Result<ObservableDefinition, ParseError> {
8240        let tok = self.consume(TokenType::Observable)?;
8241        let name = self.consume(TokenType::Identifier)?.value;
8242        let mut node = ObservableDefinition {
8243            name,
8244            qubits: None,
8245            terms: Vec::new(),
8246            loc: Loc {
8247                line: tok.line,
8248                column: tok.column,
8249            },
8250            leading_trivia: Vec::new(),
8251            trailing_trivia: Vec::new(),
8252        };
8253        self.consume(TokenType::LBrace)?;
8254        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8255            let key_tok = self.consume_any_ident_or_kw()?;
8256            self.consume(TokenType::Colon)?;
8257            match key_tok.value.as_str() {
8258                "qubits" => node.qubits = Some(self.consume_number()? as i64),
8259                "term" => {
8260                    let term_loc = Loc {
8261                        line: key_tok.line,
8262                        column: key_tok.column,
8263                    };
8264                    // Optional sign, then magnitude.
8265                    let mut negative = false;
8266                    if self.check(TokenType::Minus) {
8267                        self.advance();
8268                        negative = true;
8269                    } else if self.check(TokenType::Plus) {
8270                        self.advance();
8271                    }
8272                    let mag = self.consume_number()?;
8273                    let coefficient = if negative { -mag } else { mag };
8274                    // `*` separator between coefficient and Pauli string.
8275                    self.consume(TokenType::Star)?;
8276                    let pauli = self.consume(TokenType::StringLit)?.value;
8277                    node.terms.push(PauliTerm {
8278                        coefficient,
8279                        pauli,
8280                        loc: term_loc,
8281                    });
8282                }
8283                _ => self.skip_value(),
8284            }
8285        }
8286        self.consume(TokenType::RBrace)?;
8287        Ok(node)
8288    }
8289
8290    /// §Fase 69.a — Parse:
8291    /// `witness Name { claim: <ref>  against: <baseline>  metric: <metric>
8292    ///                 threshold: <ε>  data: <source> }`.
8293    /// Order-free `key: value` pairs. `claim`/`against`/`metric`/`data` are bare
8294    /// identifiers (a ref or a closed-catalog keyword); `threshold` is a number.
8295    /// Well-formedness (known metric, threshold range, required fields) is the
8296    /// type-checker's job (`axon-E0790`).
8297    fn parse_witness(&mut self) -> Result<WitnessDefinition, ParseError> {
8298        let tok = self.consume(TokenType::Witness)?;
8299        let name = self.consume(TokenType::Identifier)?.value;
8300        let mut node = WitnessDefinition {
8301            name,
8302            claim: String::new(),
8303            baseline: String::new(),
8304            metric: String::new(),
8305            threshold: 0.0,
8306            data: String::new(),
8307            loc: Loc {
8308                line: tok.line,
8309                column: tok.column,
8310            },
8311            leading_trivia: Vec::new(),
8312            trailing_trivia: Vec::new(),
8313        };
8314        self.consume(TokenType::LBrace)?;
8315        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8316            let key_tok = self.consume_any_ident_or_kw()?;
8317            self.consume(TokenType::Colon)?;
8318            match key_tok.value.as_str() {
8319                "claim" => node.claim = self.consume_any_ident_or_kw()?.value,
8320                // `against` is the baseline; `against` is not a reserved keyword,
8321                // so it lexes as an identifier key here.
8322                "against" => node.baseline = self.consume_any_ident_or_kw()?.value,
8323                "metric" => node.metric = self.consume_any_ident_or_kw()?.value,
8324                "threshold" => node.threshold = self.consume_number()?,
8325                "data" => node.data = self.consume_any_ident_or_kw()?.value,
8326                _ => self.skip_value(),
8327            }
8328        }
8329        self.consume(TokenType::RBrace)?;
8330        Ok(node)
8331    }
8332
8333    /// §Fase 41.b — Parse:
8334    /// `socket Name { protocol: SessionRef, backpressure: credit(n),
8335    ///               reconnect: cognitive_state, legal_basis: ... }`.
8336    /// Fields are `key: value` pairs (order-free); only `protocol` is required.
8337    fn parse_socket(&mut self) -> Result<SocketDefinition, ParseError> {
8338        let tok = self.consume(TokenType::Socket)?;
8339        let name = self.consume(TokenType::Identifier)?.value;
8340        let mut node = SocketDefinition {
8341            name,
8342            loc: Loc { line: tok.line, column: tok.column },
8343            ..Default::default()
8344        };
8345        self.consume(TokenType::LBrace)?;
8346        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8347            let key = self.consume_any_ident_or_kw()?.value;
8348            self.consume(TokenType::Colon)?;
8349            match key.as_str() {
8350                "protocol" => node.protocol = self.consume_any_ident_or_kw()?.value,
8351                "backpressure" => {
8352                    // `credit(n)` — the typed-resource window.
8353                    let kind = self.consume_any_ident_or_kw()?.value;
8354                    if kind != "credit" {
8355                        return Err(self.error(&format!("expected `credit(n)` for backpressure, got `{kind}`")));
8356                    }
8357                    self.consume(TokenType::LParen)?;
8358                    let n = self
8359                        .consume(TokenType::Integer)?
8360                        .value
8361                        .parse::<i64>()
8362                        .map_err(|_| self.error("backpressure credit must be an integer"))?;
8363                    self.consume(TokenType::RParen)?;
8364                    node.backpressure_credit = Some(n);
8365                }
8366                "reconnect" => {
8367                    let mode = self.consume_any_ident_or_kw()?.value;
8368                    node.reconnect = mode == "cognitive_state";
8369                }
8370                "legal_basis" => node.legal_basis = Some(self.consume_any_ident_or_kw()?.value),
8371                other => return Err(self.error(&format!("unknown socket field `{other}`"))),
8372            }
8373            // Optional comma between fields.
8374            if self.check(TokenType::Comma) {
8375                self.consume(TokenType::Comma)?;
8376            }
8377        }
8378        self.consume(TokenType::RBrace)?;
8379        Ok(node)
8380    }
8381
8382    /// §Fase 80.b — parse `upstream Name [from Preset@vN] { fields }`.
8383    ///
8384    /// Field grammar per `docs/fase/fase_80_upstream_design.md` §1–2. The
8385    /// parser fixes the *shape* only; catalog membership (`transport:`,
8386    /// `auth:`, `overflow:`, `on_exhausted:`), key charsets and projection
8387    /// totality are §80.c type-checker laws (T849–T851), mirroring how
8388    /// `socket` splits parse vs. check.
8389    fn parse_upstream(&mut self) -> Result<UpstreamDefinition, ParseError> {
8390        let tok = self.consume(TokenType::Upstream)?;
8391        let name = self.consume(TokenType::Identifier)?.value;
8392        let mut node = UpstreamDefinition {
8393            name,
8394            loc: Loc { line: tok.line, column: tok.column },
8395            ..Default::default()
8396        };
8397        // §80.f — preset instantiation: `upstream X from DeepgramSTT@v1 {…}`.
8398        if self.check(TokenType::From) {
8399            self.advance();
8400            let base = self.consume(TokenType::Identifier)?.value;
8401            self.consume(TokenType::At)?;
8402            let version = self.consume_any_ident_or_kw()?.value;
8403            node.preset = Some(format!("{base}@{version}"));
8404        }
8405        self.consume(TokenType::LBrace)?;
8406        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8407            let key = self.consume_any_ident_or_kw()?.value;
8408            self.consume(TokenType::Colon)?;
8409            match key.as_str() {
8410                "transport" => node.transport = self.consume_any_ident_or_kw()?.value,
8411                "protocol" => node.protocol = self.consume_any_ident_or_kw()?.value,
8412                "role" => node.role = self.consume_any_ident_or_kw()?.value,
8413                "resolve" => node.resolve = self.parse_dotted_identifier()?,
8414                // §Fase 114.u — the upstream's channel rides a declared
8415                // `resource`; the address + instance bound DERIVE from it.
8416                // XOR with `resolve:` is axon-T951 (type-checker territory).
8417                "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
8418                "secret" => node.secret = self.parse_dotted_identifier()?,
8419                "auth" => {
8420                    // `header("Name")` | `header("Name", "Prefix ")` |
8421                    // `query("param")` | `signed_url`.
8422                    node.auth_kind = self.consume_any_ident_or_kw()?.value;
8423                    if self.check(TokenType::LParen) {
8424                        self.consume(TokenType::LParen)?;
8425                        node.auth_name = Some(self.consume(TokenType::StringLit)?.value);
8426                        if self.check(TokenType::Comma) {
8427                            self.consume(TokenType::Comma)?;
8428                            node.auth_prefix = Some(self.consume(TokenType::StringLit)?.value);
8429                        }
8430                        self.consume(TokenType::RParen)?;
8431                    }
8432                }
8433                "map" => node.map = self.parse_upstream_map()?,
8434                "reconnect" => node.reconnect = Some(self.parse_upstream_reconnect()?),
8435                "overflow" => node.overflow = Some(self.consume_any_ident_or_kw()?.value),
8436                "backpressure" => {
8437                    // `credit(n)` — same typed-resource window as `socket`.
8438                    let kind = self.consume_any_ident_or_kw()?.value;
8439                    if kind != "credit" {
8440                        return Err(self.error(&format!("expected `credit(n)` for backpressure, got `{kind}`")));
8441                    }
8442                    self.consume(TokenType::LParen)?;
8443                    let n = self
8444                        .consume(TokenType::Integer)?
8445                        .value
8446                        .parse::<i64>()
8447                        .map_err(|_| self.error("backpressure credit must be an integer"))?;
8448                    self.consume(TokenType::RParen)?;
8449                    node.backpressure_credit = Some(n);
8450                }
8451                other => return Err(self.error(&format!("unknown upstream field `{other}`"))),
8452            }
8453            // Optional comma between fields.
8454            if self.check(TokenType::Comma) {
8455                self.consume(TokenType::Comma)?;
8456            }
8457        }
8458        self.consume(TokenType::RBrace)?;
8459        Ok(node)
8460    }
8461
8462    /// §Fase 83.a — parse `cors Name { fields }`. Field-shape checks
8463    /// (wildcard+credentials, origin-glob shape, closed method catalog,
8464    /// cross-method path consistency) are §83.c type-checker territory
8465    /// (T853-T857); the parser only builds the structural AST.
8466    ///
8467    /// **Unknown fields are a hard error** (D83.7, not `shield`'s lenient
8468    /// `axon-W010` record-and-skip) — mirrors `upstream`'s stricter
8469    /// posture, appropriate for a security-relevant declaration.
8470    fn parse_cors(&mut self) -> Result<CorsDefinition, ParseError> {
8471        let tok = self.consume(TokenType::Cors)?;
8472        let name = self.consume(TokenType::Identifier)?.value;
8473        let mut node = CorsDefinition {
8474            name,
8475            loc: Loc { line: tok.line, column: tok.column },
8476            ..Default::default()
8477        };
8478        self.consume(TokenType::LBrace)?;
8479        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8480            let key = self.consume_any_ident_or_kw()?.value;
8481            self.consume(TokenType::Colon)?;
8482            match key.as_str() {
8483                "allow_origins" => node.allow_origins = self.parse_bracketed_strings()?,
8484                "allow_methods" => node.allow_methods = self.parse_bracketed_identifiers()?,
8485                "allow_headers" => node.allow_headers = self.parse_bracketed_strings()?,
8486                "allow_credentials" => {
8487                    node.allow_credentials = self.consume_any_ident_or_kw()?.value == "true"
8488                }
8489                "max_age" => node.max_age = Some(self.consume(TokenType::Duration)?.value),
8490                "expose_headers" => node.expose_headers = self.parse_bracketed_strings()?,
8491                other => return Err(self.error(&format!("unknown cors field `{other}`"))),
8492            }
8493            // Optional comma between fields.
8494            if self.check(TokenType::Comma) {
8495                self.consume(TokenType::Comma)?;
8496            }
8497        }
8498        self.consume(TokenType::RBrace)?;
8499        Ok(node)
8500    }
8501
8502    /// §Fase 92.a — parse `credential Name { ttl: grants: }`. Strict
8503    /// closed-catalog (unknown field is a hard error, the §83 D83.7
8504    /// discipline — a credential contract governs AUTHORITY, so a typo can
8505    /// never silently produce a permissive contract). `grants:` slugs are
8506    /// validated at parse time with the same closed grammar as
8507    /// `axonendpoint requires:`; the cross-field laws (non-empty grants,
8508    /// TTL bounds) are §92.a type-checker territory (`axon-T893`/`T894`).
8509    fn parse_credential(&mut self) -> Result<CredentialDefinition, ParseError> {
8510        let tok = self.consume(TokenType::Credential)?;
8511        let name = self.consume(TokenType::Identifier)?.value;
8512        let mut node = CredentialDefinition {
8513            name,
8514            loc: Loc { line: tok.line, column: tok.column },
8515            ..Default::default()
8516        };
8517        self.consume(TokenType::LBrace)?;
8518        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8519            let key = self.consume_any_ident_or_kw()?.value;
8520            self.consume(TokenType::Colon)?;
8521            match key.as_str() {
8522                "ttl" => node.ttl = self.consume(TokenType::Duration)?.value,
8523                "grants" => {
8524                    let bracket_tok = self.current().clone();
8525                    let items = self.parse_bracketed_dot_identifiers()?;
8526                    for slug in &items {
8527                        if !is_valid_capability_slug(slug) {
8528                            return Err(ParseError {
8529                                message: format!(
8530                                    "Invalid capability slug '{slug}' in credential '{}' \
8531                                     `grants:`. Capability slugs must match \
8532                                     ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
8533                                     lowercase identifiers starting with a letter. Examples: \
8534                                     `chat.invoke`, `flow.execute`.",
8535                                    node.name
8536                                ),
8537                                line: bracket_tok.line,
8538                                column: bracket_tok.column,
8539                                ..Default::default()
8540                            });
8541                        }
8542                    }
8543                    node.grants = items;
8544                }
8545                other => return Err(self.error(&format!("unknown credential field `{other}`"))),
8546            }
8547            // Optional comma between fields.
8548            if self.check(TokenType::Comma) {
8549                self.consume(TokenType::Comma)?;
8550            }
8551        }
8552        self.consume(TokenType::RBrace)?;
8553        Ok(node)
8554    }
8555
8556    /// §Fase 85.a — parse `cache Name { backend:, ttl:, key:, default:,
8557    /// apply_to_effects:, invalidate_on: }`. Strict closed-catalog (unknown
8558    /// field is a hard error, the §83 D83.7 discipline — a cache governs
8559    /// correctness, so a typo can never silently mean "no policy"). All
8560    /// cross-field laws (single default, non-pure-needs-ttl, reference
8561    /// resolution, effect widening) are §85.c type-checker territory.
8562    fn parse_cache(&mut self) -> Result<CacheDefinition, ParseError> {
8563        let tok = self.consume(TokenType::Cache)?;
8564        let name = self.consume(TokenType::Identifier)?.value;
8565        let mut node = CacheDefinition {
8566            name,
8567            loc: Loc { line: tok.line, column: tok.column },
8568            ..Default::default()
8569        };
8570        self.consume(TokenType::LBrace)?;
8571        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8572            let key = self.consume_any_ident_or_kw()?.value;
8573            self.consume(TokenType::Colon)?;
8574            match key.as_str() {
8575                "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
8576                "ttl" => node.ttl = Some(self.consume(TokenType::Duration)?.value),
8577                "key" => node.key_params = self.parse_bracketed_identifiers()?,
8578                "default" => {
8579                    node.default_policy = self.consume_any_ident_or_kw()?.value == "true"
8580                }
8581                "apply_to_effects" => {
8582                    node.apply_to_effects = self.parse_bracketed_identifiers()?
8583                }
8584                "invalidate_on" => node.invalidate_on = self.parse_bracketed_identifiers()?,
8585                other => return Err(self.error(&format!("unknown cache field `{other}`"))),
8586            }
8587            if self.check(TokenType::Comma) {
8588                self.consume(TokenType::Comma)?;
8589            }
8590        }
8591        self.consume(TokenType::RBrace)?;
8592        Ok(node)
8593    }
8594
8595    // ── §Fase 99.b — Native Document Synthesis ─────────────────────────────
8596
8597    /// §Fase 99.b — parse `document <Name> { target:, template:?, provenance:?,
8598    /// effects:?, <body blocks> }`. Document-level scalars are handled here;
8599    /// anything of the form `ident { … }` is a body block ([`parse_doc_block_body`]).
8600    /// Unknown scalar fields are a hard error (the §83/§84 closed-catalog
8601    /// discipline); the per-`target` block vocabulary is the §99.c checker's job.
8602    fn parse_document(&mut self) -> Result<crate::ast::DocumentDefinition, ParseError> {
8603        let tok = self.consume(TokenType::Document)?;
8604        let name = self.consume(TokenType::Identifier)?.value;
8605        let mut node = crate::ast::DocumentDefinition {
8606            name,
8607            loc: Loc {
8608                line: tok.line,
8609                column: tok.column,
8610            },
8611            ..Default::default()
8612        };
8613        self.consume(TokenType::LBrace)?;
8614        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8615            let field = self.current().clone();
8616            let field_name = field.value.clone();
8617            self.advance();
8618            if self.check(TokenType::Colon) {
8619                self.advance();
8620                match field_name.as_str() {
8621                    "target" => node.target = self.consume_any_ident_or_kw()?.value,
8622                    "template" => node.template = self.parse_dotted_identifier()?,
8623                    "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
8624                    "effects" => node.effects = Some(self.parse_effect_row()?),
8625                    other => {
8626                        return Err(self.error(&format!(
8627                            "unknown document field `{other}` in document `{}` — expected \
8628                             `target:` / `template:` / `provenance:` / `effects:`, or a body \
8629                             block (`section {{ … }}` / `slide {{ … }}` / `sheet {{ … }}`)",
8630                            node.name
8631                        )))
8632                    }
8633                }
8634            } else if self.check(TokenType::LBrace) {
8635                node.blocks
8636                    .push(self.parse_doc_block_body(field_name, field.line, field.column)?);
8637            } else {
8638                return Err(self.error(&format!(
8639                    "unexpected `{field_name}` in document `{}` body — expected a `field:` or a \
8640                     body block `{field_name} {{ … }}`",
8641                    node.name
8642                )));
8643            }
8644            if self.check(TokenType::Comma) {
8645                self.advance();
8646            }
8647        }
8648        self.consume(TokenType::RBrace)?;
8649        Ok(node)
8650    }
8651
8652    /// §Fase 99.b — parse a document body block whose `kind` was already
8653    /// consumed: `{ (field: value | nested-block { … })* }`. Recursive — a
8654    /// `section` holds `para`/`table`/`chart`; a `slide` holds `bullets`/
8655    /// `notes`; a `sheet` holds `row`/`formula`. A member is a field iff a
8656    /// `:` follows its name; else it must open a nested block (`{`).
8657    fn parse_doc_block_body(
8658        &mut self,
8659        kind: String,
8660        line: u32,
8661        column: u32,
8662    ) -> Result<crate::ast::DocBlock, ParseError> {
8663        let mut block = crate::ast::DocBlock {
8664            kind,
8665            loc: Loc { line, column },
8666            ..Default::default()
8667        };
8668        self.consume(TokenType::LBrace)?;
8669        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8670            let name_tok = self.current().clone();
8671            let name = self.consume_any_ident_or_kw()?.value;
8672            if self.check(TokenType::Colon) {
8673                self.advance();
8674                let value = self.parse_doc_scalar()?;
8675                block.fields.push((name, value));
8676            } else if self.check(TokenType::LBrace) {
8677                let child = self.parse_doc_block_body(name, name_tok.line, name_tok.column)?;
8678                block.children.push(child);
8679            } else {
8680                return Err(self.error(&format!(
8681                    "in document block `{}`: `{name}` must be a `field:` value or open a nested \
8682                     block `{name} {{ … }}`",
8683                    block.kind
8684                )));
8685            }
8686            if self.check(TokenType::Comma) {
8687                self.advance();
8688            }
8689        }
8690        self.consume(TokenType::RBrace)?;
8691        Ok(block)
8692    }
8693
8694    /// §Fase 99.b — parse a document field value into a [`crate::ast::DocScalar`].
8695    /// A bare identifier is a REFERENCE (`text: revenue_summary`) — this is what
8696    /// the assertion-laundering barrier inspects; a quoted string / int / bool /
8697    /// bracketed list are literals.
8698    fn parse_doc_scalar(&mut self) -> Result<crate::ast::DocScalar, ParseError> {
8699        let tok = self.current().clone();
8700        match tok.ttype {
8701            TokenType::StringLit => {
8702                self.advance();
8703                Ok(crate::ast::DocScalar::Text(tok.value))
8704            }
8705            TokenType::Integer => {
8706                self.advance();
8707                Ok(crate::ast::DocScalar::Int(tok.value.parse::<i64>().unwrap_or(0)))
8708            }
8709            TokenType::Bool => {
8710                self.advance();
8711                Ok(crate::ast::DocScalar::Bool(tok.value == "true"))
8712            }
8713            TokenType::LBracket => {
8714                let items = self.parse_bracketed_strings()?;
8715                Ok(crate::ast::DocScalar::List(items))
8716            }
8717            _ => {
8718                let name = self.consume_any_ident_or_kw()?.value;
8719                Ok(crate::ast::DocScalar::Ref(name))
8720            }
8721        }
8722    }
8723
8724    // ── §Fase 105 — Governed CRM Delivery ──────────────────────────────────
8725
8726    /// §Fase 105 — parse `deliver <Name> { target:, provenance:?, secret:,
8727    /// effects:?, <operation blocks> }`. Delivery-level scalars are handled here;
8728    /// anything of the form `ident { … }` is an operation block
8729    /// ([`parse_deliver_op`]). Unknown scalar fields are a hard error (the §99
8730    /// §Fase 110.a — the governed human-notification declaration:
8731    ///
8732    /// ```text
8733    /// notify LowSales {
8734    ///     channel:    sms | whatsapp | telegram
8735    ///     to:         secret(ops.oncall_phone)
8736    ///     template:   "Ventas 7d: ${resumen}"
8737    ///     window:     4h
8738    ///     provenance: attached | cleared
8739    ///     effects:    <web>
8740    /// }
8741    /// ```
8742    ///
8743    /// The closed-field discipline (§99/§105): an unknown scalar field is
8744    /// a hard parse error. The LAWS (T933/T934/T935) live in the checker
8745    /// so violations accumulate; the parser records shape (including a
8746    /// literal `to:` — kept so T934 can refuse it TEACHING the custody
8747    /// form, instead of a bare parse error).
8748    fn parse_notify(&mut self) -> Result<crate::ast::NotifyDefinition, ParseError> {
8749        let tok = self.consume(TokenType::Notify)?;
8750        let name = self.consume(TokenType::Identifier)?.value;
8751        let mut node = crate::ast::NotifyDefinition {
8752            name,
8753            loc: Loc {
8754                line: tok.line,
8755                column: tok.column,
8756            },
8757            ..Default::default()
8758        };
8759        self.consume(TokenType::LBrace)?;
8760        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8761            let field = self.current().clone();
8762            let field_name = field.value.clone();
8763            self.advance();
8764            if self.check(TokenType::Colon) {
8765                self.advance();
8766                match field_name.as_str() {
8767                    "channel" => node.channel = self.consume_any_ident_or_kw()?.value,
8768                    "to" => {
8769                        // The custody form: `secret(<dotted-class>)`. A string
8770                        // literal parses too — the checker refuses it (T934)
8771                        // with the teaching message.
8772                        if self.current().value == "secret" && self.peek_is_lparen() {
8773                            self.advance(); // `secret`
8774                            self.consume(TokenType::LParen)?;
8775                            node.to_secret = self.parse_dotted_identifier()?;
8776                            self.consume(TokenType::RParen)?;
8777                            node.to_is_secret = true;
8778                        } else if self.check(TokenType::StringLit) {
8779                            node.to_secret = self.consume(TokenType::StringLit)?.value.clone();
8780                            node.to_is_secret = false;
8781                        } else {
8782                            node.to_secret = self.consume_any_ident_or_kw()?.value.clone();
8783                            node.to_is_secret = false;
8784                        }
8785                    }
8786                    "template" => {
8787                        node.template = self.consume(TokenType::StringLit)?.value.clone()
8788                    }
8789                    "window" => {
8790                        // `4h` lexes as Integer + ident or one ident — accept
8791                        // both spellings, normalized to the joined form.
8792                        if self.check(TokenType::Integer) {
8793                            let n = self.consume(TokenType::Integer)?.value.clone();
8794                            let unit = self.consume_any_ident_or_kw()?.value.clone();
8795                            node.window = format!("{n}{unit}");
8796                        } else {
8797                            node.window = self.consume_any_ident_or_kw()?.value.clone();
8798                        }
8799                    }
8800                    "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
8801                    "effects" => node.effects = Some(self.parse_effect_row()?),
8802                    other => {
8803                        return Err(self.error(&format!(
8804                            "unknown notify field `{other}` in notify `{}` — expected \
8805                             `channel:` / `to:` / `template:` / `window:` / `provenance:` / \
8806                             `effects:`",
8807                            node.name
8808                        )))
8809                    }
8810                }
8811            }
8812        }
8813        self.consume(TokenType::RBrace)?;
8814        Ok(node)
8815    }
8816
8817    /// §Fase 110.a — one-token lookahead helper for the `secret(` form.
8818    /// §Fase 114.a — is the NEXT token an identifier? (`budget <Name> { … }` vs
8819    /// a bare `budget` used as an ordinary identifier.)
8820    fn peek_is_identifier(&self) -> bool {
8821        self.tokens
8822            .get(self.pos + 1)
8823            .map(|t| t.ttype == TokenType::Identifier)
8824            .unwrap_or(false)
8825    }
8826
8827    fn peek_is_lparen(&self) -> bool {
8828        self.tokens
8829            .get(self.pos + 1)
8830            .map(|t| t.ttype == TokenType::LParen)
8831            .unwrap_or(false)
8832    }
8833
8834    /// closed-catalog discipline); the operation vocabulary is the checker's job.
8835    fn parse_deliver(&mut self) -> Result<crate::ast::DeliverDefinition, ParseError> {
8836        let tok = self.consume(TokenType::Deliver)?;
8837        let name = self.consume(TokenType::Identifier)?.value;
8838        let mut node = crate::ast::DeliverDefinition {
8839            name,
8840            loc: Loc {
8841                line: tok.line,
8842                column: tok.column,
8843            },
8844            ..Default::default()
8845        };
8846        self.consume(TokenType::LBrace)?;
8847        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8848            let field = self.current().clone();
8849            let field_name = field.value.clone();
8850            self.advance();
8851            if self.check(TokenType::Colon) {
8852                self.advance();
8853                match field_name.as_str() {
8854                    "target" => node.target = self.consume_any_ident_or_kw()?.value,
8855                    "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
8856                    "secret" => node.secret = self.consume_any_ident_or_kw()?.value,
8857                    "effects" => node.effects = Some(self.parse_effect_row()?),
8858                    other => {
8859                        return Err(self.error(&format!(
8860                            "unknown deliver field `{other}` in deliver `{}` — expected \
8861                             `target:` / `provenance:` / `secret:` / `effects:`, or an operation \
8862                             block (`upsert_contact {{ … }}` / `create_deal {{ … }}` / \
8863                             `add_note {{ … }}`)",
8864                            node.name
8865                        )))
8866                    }
8867                }
8868            } else if self.check(TokenType::LBrace) {
8869                node.ops
8870                    .push(self.parse_deliver_op(field_name, field.line, field.column)?);
8871            } else {
8872                return Err(self.error(&format!(
8873                    "unexpected `{field_name}` in deliver `{}` body — expected a `field:` or an \
8874                     operation block `{field_name} {{ … }}`",
8875                    node.name
8876                )));
8877            }
8878            if self.check(TokenType::Comma) {
8879                self.advance();
8880            }
8881        }
8882        self.consume(TokenType::RBrace)?;
8883        Ok(node)
8884    }
8885
8886    /// §Fase 105 — parse a delivery operation block whose `kind` was already
8887    /// consumed: `{ (field: value)* }`. Flat (unlike a document block, an
8888    /// operation has no nested children) — each member must be a `field: value`.
8889    fn parse_deliver_op(
8890        &mut self,
8891        kind: String,
8892        line: u32,
8893        column: u32,
8894    ) -> Result<crate::ast::DeliverOp, ParseError> {
8895        let mut op = crate::ast::DeliverOp {
8896            kind,
8897            loc: Loc { line, column },
8898            ..Default::default()
8899        };
8900        self.consume(TokenType::LBrace)?;
8901        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8902            let name = self.consume_any_ident_or_kw()?.value;
8903            self.consume(TokenType::Colon).map_err(|_| {
8904                self.error(&format!(
8905                    "in deliver operation `{}`: `{name}` must be a `field: value` pair — an \
8906                     operation binds scalar fields, it takes no nested blocks",
8907                    op.kind
8908                ))
8909            })?;
8910            let value = self.parse_doc_scalar()?;
8911            op.fields.push((name, value));
8912            if self.check(TokenType::Comma) {
8913                self.advance();
8914            }
8915        }
8916        self.consume(TokenType::RBrace)?;
8917        Ok(op)
8918    }
8919
8920    /// §Fase 87.a — parse `savant <Name> { domain:, cognition{…}, memory{…},
8921    /// budget{…}, mandate <M> {…} … }`. The block surface only; catalog +
8922    /// ref-resolution + budget/interruptibility binding is the §87.b/c checker's
8923    /// job (the standing parse/check split). Unknown fields are a hard error
8924    /// (D83.7): a savant governs an expensive autonomous process.
8925    fn parse_savant(&mut self) -> Result<SavantDefinition, ParseError> {
8926        let tok = self.consume(TokenType::Savant)?;
8927        let name = self.consume(TokenType::Identifier)?.value;
8928        let mut node = SavantDefinition {
8929            name,
8930            loc: Loc {
8931                line: tok.line,
8932                column: tok.column,
8933            },
8934            ..Default::default()
8935        };
8936        self.consume(TokenType::LBrace)?;
8937        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8938            let field = self.current().clone();
8939            let field_name = field.value.clone();
8940            self.advance();
8941            if self.check(TokenType::Colon) {
8942                self.advance();
8943                match field_name.as_str() {
8944                    "domain" => node.domain = self.consume(TokenType::StringLit)?.value,
8945                    other => {
8946                        return Err(self.error(&format!(
8947                            "unknown savant field `{other}` in savant `{}` — expected \
8948                             `domain:` or a `cognition` / `memory` / `budget` / `mandate` block",
8949                            node.name
8950                        )))
8951                    }
8952                }
8953            } else if field_name == "cognition" {
8954                node.cognition = Some(self.parse_savant_cognition(field.line, field.column)?);
8955            } else if field_name == "memory" {
8956                node.memory = Some(self.parse_savant_memory(field.line, field.column)?);
8957            } else if field_name == "budget" {
8958                node.budget = Some(self.parse_savant_budget(field.line, field.column)?);
8959            } else if field_name == "mandate" {
8960                node.mandates
8961                    .push(self.parse_savant_mandate(field.line, field.column)?);
8962            } else {
8963                return Err(self.error(&format!(
8964                    "unexpected `{field_name}` in savant `{}` body — expected `domain:` or a \
8965                     `cognition` / `memory` / `budget` / `mandate` block",
8966                    node.name
8967                )));
8968            }
8969            if self.check(TokenType::Comma) {
8970                self.advance();
8971            }
8972        }
8973        self.consume(TokenType::RBrace)?;
8974        Ok(node)
8975    }
8976
8977    /// §Fase 87.a — the `cognition { depth:, entropic_threshold:, divergence: }`
8978    /// sub-block. Catalog validation of `depth`/`divergence` is §87.b.
8979    fn parse_savant_cognition(
8980        &mut self,
8981        line: u32,
8982        column: u32,
8983    ) -> Result<SavantCognition, ParseError> {
8984        self.consume(TokenType::LBrace)?;
8985        let mut node = SavantCognition {
8986            loc: Loc { line, column },
8987            ..Default::default()
8988        };
8989        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8990            let key = self.consume_any_ident_or_kw()?.value;
8991            self.consume(TokenType::Colon)?;
8992            match key.as_str() {
8993                "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
8994                "entropic_threshold" => node.entropic_threshold = self.parse_optional_float(),
8995                "divergence" => node.divergence = self.consume_any_ident_or_kw()?.value,
8996                other => {
8997                    return Err(self.error(&format!(
8998                        "unknown savant `cognition` field `{other}` — expected \
8999                         `depth` / `entropic_threshold` / `divergence`"
9000                    )))
9001                }
9002            }
9003            if self.check(TokenType::Comma) {
9004                self.advance();
9005            }
9006        }
9007        self.consume(TokenType::RBrace)?;
9008        Ok(node)
9009    }
9010
9011    /// §Fase 87.a — the `memory { backend:, corpus_graph:, isolation_level: }`
9012    /// sub-block. `backend` is resolved to a declared `memory`/`corpus` in §87.c.
9013    fn parse_savant_memory(
9014        &mut self,
9015        line: u32,
9016        column: u32,
9017    ) -> Result<SavantMemory, ParseError> {
9018        self.consume(TokenType::LBrace)?;
9019        let mut node = SavantMemory {
9020            loc: Loc { line, column },
9021            ..Default::default()
9022        };
9023        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9024            let key = self.consume_any_ident_or_kw()?.value;
9025            self.consume(TokenType::Colon)?;
9026            match key.as_str() {
9027                "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
9028                "corpus_graph" => {
9029                    node.corpus_graph = self.consume_any_ident_or_kw()?.value == "true"
9030                }
9031                "isolation_level" => node.isolation_level = self.consume_any_ident_or_kw()?.value,
9032                other => {
9033                    return Err(self.error(&format!(
9034                        "unknown savant `memory` field `{other}` — expected \
9035                         `backend` / `corpus_graph` / `isolation_level`"
9036                    )))
9037                }
9038            }
9039            if self.check(TokenType::Comma) {
9040                self.advance();
9041            }
9042        }
9043        self.consume(TokenType::RBrace)?;
9044        Ok(node)
9045    }
9046
9047    /// §Fase 87.a — the `budget { max_iterations:, max_tool_synth: }` sub-block.
9048    /// Bound to a §72 linear budget (`RateLease`) in §87.c.
9049    fn parse_savant_budget(
9050        &mut self,
9051        line: u32,
9052        column: u32,
9053    ) -> Result<SavantBudget, ParseError> {
9054        self.consume(TokenType::LBrace)?;
9055        let mut node = SavantBudget {
9056            loc: Loc { line, column },
9057            ..Default::default()
9058        };
9059        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9060            let key = self.consume_any_ident_or_kw()?.value;
9061            self.consume(TokenType::Colon)?;
9062            match key.as_str() {
9063                "max_iterations" => node.max_iterations = self.parse_optional_int(),
9064                "max_tool_synth" => node.max_tool_synth = self.parse_optional_int(),
9065                other => {
9066                    return Err(self.error(&format!(
9067                        "unknown savant `budget` field `{other}` — expected \
9068                         `max_iterations` / `max_tool_synth`"
9069                    )))
9070                }
9071            }
9072            if self.check(TokenType::Comma) {
9073                self.advance();
9074            }
9075        }
9076        self.consume(TokenType::RBrace)?;
9077        Ok(node)
9078    }
9079
9080    /// §Fase 87.a — the `mandate <Name> { objective:, output: }` sub-block. The
9081    /// `mandate` keyword is already consumed by `parse_savant`.
9082    fn parse_savant_mandate(
9083        &mut self,
9084        line: u32,
9085        column: u32,
9086    ) -> Result<SavantMandate, ParseError> {
9087        let name = self.consume(TokenType::Identifier)?.value;
9088        let mut node = SavantMandate {
9089            name,
9090            loc: Loc { line, column },
9091            ..Default::default()
9092        };
9093        self.consume(TokenType::LBrace)?;
9094        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9095            let key = self.consume_any_ident_or_kw()?.value;
9096            self.consume(TokenType::Colon)?;
9097            match key.as_str() {
9098                "objective" => node.objective = self.consume(TokenType::StringLit)?.value,
9099                "output" => node.output_type = self.consume_any_ident_or_kw()?.value,
9100                other => {
9101                    return Err(self.error(&format!(
9102                        "unknown savant `mandate` field `{other}` — expected `objective` / `output`"
9103                    )))
9104                }
9105            }
9106            if self.check(TokenType::Comma) {
9107                self.advance();
9108            }
9109        }
9110        self.consume(TokenType::RBrace)?;
9111        Ok(node)
9112    }
9113
9114    /// §Fase 87.d — parse `synth <Name> { target:, risk:, language:, sandbox:,
9115    /// review:, max_lines: }`. Flat key:value block (the `cache` shape). Catalog
9116    /// + deny-by-default validation is §87.d `check_synth`. Unknown fields are a
9117    /// hard error (D83.7): a synth policy governs arbitrary-code execution.
9118    fn parse_synth(&mut self) -> Result<SynthDefinition, ParseError> {
9119        let tok = self.consume(TokenType::Synth)?;
9120        let name = self.consume(TokenType::Identifier)?.value;
9121        let mut node = SynthDefinition {
9122            name,
9123            loc: Loc {
9124                line: tok.line,
9125                column: tok.column,
9126            },
9127            ..Default::default()
9128        };
9129        self.consume(TokenType::LBrace)?;
9130        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9131            let key = self.consume_any_ident_or_kw()?.value;
9132            self.consume(TokenType::Colon)?;
9133            match key.as_str() {
9134                "target" => node.target = self.consume(TokenType::StringLit)?.value,
9135                "risk" => node.risk = self.consume_any_ident_or_kw()?.value,
9136                "language" => node.language = self.consume_any_ident_or_kw()?.value,
9137                "sandbox" => node.sandbox = self.consume_any_ident_or_kw()?.value,
9138                "review" => node.review = self.consume_any_ident_or_kw()?.value,
9139                "max_lines" => node.max_lines = self.parse_optional_int(),
9140                other => {
9141                    return Err(self.error(&format!(
9142                        "unknown synth field `{other}` in synth `{}` — expected `target` / `risk` \
9143                         / `language` / `sandbox` / `review` / `max_lines`",
9144                        node.name
9145                    )))
9146                }
9147            }
9148            if self.check(TokenType::Comma) {
9149                self.consume(TokenType::Comma)?;
9150            }
9151        }
9152        self.consume(TokenType::RBrace)?;
9153        Ok(node)
9154    }
9155
9156    /// §Fase 80.g — parse `voice Name { fields }`. Cross-field laws
9157    /// (stt/tts XOR realtime, interruptible ⇒ legal_basis, ref resolution)
9158    /// are §80.c type-checker territory (T852), same parse/check split as
9159    /// every primitive in this file.
9160    fn parse_voice(&mut self) -> Result<VoiceDefinition, ParseError> {
9161        let tok = self.consume(TokenType::Voice)?;
9162        let name = self.consume(TokenType::Identifier)?.value;
9163        let mut node = VoiceDefinition {
9164            name,
9165            loc: Loc { line: tok.line, column: tok.column },
9166            ..Default::default()
9167        };
9168        self.consume(TokenType::LBrace)?;
9169        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9170            let key = self.consume_any_ident_or_kw()?.value;
9171            self.consume(TokenType::Colon)?;
9172            match key.as_str() {
9173                // Each leg: a declared upstream name or a `Preset@vN` ref.
9174                "stt" => node.stt = Some(self.parse_upstream_ref()?),
9175                "tts" => node.tts = Some(self.parse_upstream_ref()?),
9176                "realtime" => node.realtime = Some(self.parse_upstream_ref()?),
9177                "carrier" => node.carrier = self.consume_any_ident_or_kw()?.value,
9178                "interruptible" => {
9179                    let v = self.consume_any_ident_or_kw()?.value;
9180                    node.interruptible = v == "true";
9181                }
9182                "legal_basis" => node.legal_basis = Some(self.consume_any_ident_or_kw()?.value),
9183                "persona" => node.persona = Some(self.consume(TokenType::Identifier)?.value),
9184                "context" => node.context = Some(self.consume(TokenType::Identifier)?.value),
9185                other => return Err(self.error(&format!("unknown voice field `{other}`"))),
9186            }
9187            if self.check(TokenType::Comma) {
9188                self.consume(TokenType::Comma)?;
9189            }
9190        }
9191        self.consume(TokenType::RBrace)?;
9192        Ok(node)
9193    }
9194
9195    /// §Fase 80.g — an upstream leg reference: `Ident` (a declared
9196    /// `upstream`) or `Ident@vN` (a §80.f preset).
9197    fn parse_upstream_ref(&mut self) -> Result<String, ParseError> {
9198        let base = self.consume(TokenType::Identifier)?.value;
9199        if self.check(TokenType::At) {
9200            self.advance();
9201            let version = self.consume_any_ident_or_kw()?.value;
9202            Ok(format!("{base}@{version}"))
9203        } else {
9204            Ok(base)
9205        }
9206    }
9207
9208    /// §Fase 80.b — parse the `map: [ rule, … ]` projection list.
9209    ///
9210    /// rule := (`send` | `receive`) <MessageType> `as` (`json` | `binary`)
9211    ///         [ `tag` <string> ]                 — send-json only
9212    ///         [ `when` <string> `=` <string> ]   — receive-json only
9213    fn parse_upstream_map(&mut self) -> Result<Vec<UpstreamMapRule>, ParseError> {
9214        self.consume(TokenType::LBracket)?;
9215        let mut rules = Vec::new();
9216        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
9217            let dir_tok = self.current().clone();
9218            let direction = match dir_tok.ttype {
9219                TokenType::Send => "send",
9220                TokenType::Receive => "receive",
9221                _ => {
9222                    return Err(self.error(&format!(
9223                        "upstream map rule must start with `send` or `receive`, got `{}`",
9224                        dir_tok.value
9225                    )))
9226                }
9227            };
9228            self.advance();
9229            let message = self.consume(TokenType::Identifier)?.value;
9230            self.consume(TokenType::As)?;
9231            let framing = self.consume_any_ident_or_kw()?.value;
9232            let mut rule = UpstreamMapRule {
9233                direction: direction.to_string(),
9234                message,
9235                framing,
9236                loc: Loc { line: dir_tok.line, column: dir_tok.column },
9237                ..Default::default()
9238            };
9239            // Optional selectors — contextual identifiers, not keywords.
9240            if self.current().value == "tag" {
9241                self.advance();
9242                rule.tag = Some(self.consume(TokenType::StringLit)?.value);
9243            } else if self.current().value == "when" {
9244                // `when "f" = "v"` — equality discriminator; `when "f"` —
9245                // field-PRESENCE discriminator (vendors like Gemini Live /
9246                // ElevenLabs mark frame kinds by which key exists, not by a
9247                // type value).
9248                self.advance();
9249                rule.when_field = Some(self.consume(TokenType::StringLit)?.value);
9250                if self.check(TokenType::Assign) {
9251                    self.advance();
9252                    rule.when_value = Some(self.consume(TokenType::StringLit)?.value);
9253                }
9254            }
9255            rules.push(rule);
9256            if self.check(TokenType::Comma) {
9257                self.advance();
9258            }
9259        }
9260        self.consume(TokenType::RBracket)?;
9261        Ok(rules)
9262    }
9263
9264    /// §Fase 80.b — parse `reconnect: { backoff_ms: <int>, max_attempts:
9265    /// <int>, on_exhausted: <ident> }` (order-free, all three required —
9266    /// a reconnection policy with a hole is not a policy).
9267    fn parse_upstream_reconnect(&mut self) -> Result<UpstreamReconnect, ParseError> {
9268        self.consume(TokenType::LBrace)?;
9269        let mut backoff_ms: Option<i64> = None;
9270        let mut max_attempts: Option<i64> = None;
9271        let mut on_exhausted: Option<String> = None;
9272        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9273            let key = self.consume_any_ident_or_kw()?.value;
9274            self.consume(TokenType::Colon)?;
9275            match key.as_str() {
9276                "backoff_ms" => {
9277                    backoff_ms = Some(
9278                        self.consume(TokenType::Integer)?
9279                            .value
9280                            .parse::<i64>()
9281                            .map_err(|_| self.error("backoff_ms must be an integer"))?,
9282                    )
9283                }
9284                "max_attempts" => {
9285                    max_attempts = Some(
9286                        self.consume(TokenType::Integer)?
9287                            .value
9288                            .parse::<i64>()
9289                            .map_err(|_| self.error("max_attempts must be an integer"))?,
9290                    )
9291                }
9292                "on_exhausted" => on_exhausted = Some(self.consume_any_ident_or_kw()?.value),
9293                other => return Err(self.error(&format!("unknown reconnect field `{other}`"))),
9294            }
9295            if self.check(TokenType::Comma) {
9296                self.consume(TokenType::Comma)?;
9297            }
9298        }
9299        self.consume(TokenType::RBrace)?;
9300        match (backoff_ms, max_attempts, on_exhausted) {
9301            (Some(b), Some(m), Some(o)) => Ok(UpstreamReconnect { backoff_ms: b, max_attempts: m, on_exhausted: o }),
9302            _ => Err(self.error(
9303                "reconnect requires all of `backoff_ms:`, `max_attempts:`, `on_exhausted:` — a reconnection policy with a hole is not a policy",
9304            )),
9305        }
9306    }
9307
9308    /// Parse: `[send T, receive U, loop, end]`.
9309    fn parse_session_steps(&mut self) -> Result<Vec<SessionStep>, ParseError> {
9310        self.consume(TokenType::LBracket)?;
9311        let mut steps = Vec::new();
9312        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
9313            steps.push(self.parse_session_step()?);
9314            if self.check(TokenType::Comma) {
9315                self.advance();
9316            }
9317        }
9318        self.consume(TokenType::RBracket)?;
9319        Ok(steps)
9320    }
9321
9322    /// §Fase 79.b — a **brace**-delimited session step block: `{ step, step, … }`.
9323    /// Used by the `interrupt`/`resumable` regions (the paper's block surface),
9324    /// as opposed to the `[ … ]` step-lists used by roles and choice arms.
9325    fn parse_session_step_block(&mut self) -> Result<Vec<SessionStep>, ParseError> {
9326        self.consume(TokenType::LBrace)?;
9327        let mut steps = Vec::new();
9328        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9329            steps.push(self.parse_session_step()?);
9330            if self.check(TokenType::Comma) {
9331                self.advance();
9332            }
9333        }
9334        self.consume(TokenType::RBrace)?;
9335        Ok(steps)
9336    }
9337
9338    fn parse_session_step(&mut self) -> Result<SessionStep, ParseError> {
9339        let tok = self.current().clone();
9340        let loc = Loc { line: tok.line, column: tok.column };
9341        match tok.ttype {
9342            TokenType::Send => {
9343                self.advance();
9344                let msg = self.consume_any_ident_or_kw()?;
9345                Ok(SessionStep { op: "send".into(), message_type: msg.value, loc, ..Default::default() })
9346            }
9347            TokenType::Receive => {
9348                self.advance();
9349                let msg = self.consume_any_ident_or_kw()?;
9350                Ok(SessionStep { op: "receive".into(), message_type: msg.value, loc, ..Default::default() })
9351            }
9352            TokenType::Loop => {
9353                self.advance();
9354                Ok(SessionStep { op: "loop".into(), loc, ..Default::default() })
9355            }
9356            TokenType::End => {
9357                self.advance();
9358                Ok(SessionStep { op: "end".into(), loc, ..Default::default() })
9359            }
9360            // §Fase 41.b — choice: `select { ℓ: [..], … }` (⊕) | `branch { ℓ: [..], … }` (&).
9361            // `select`/`branch` are not keywords — they arrive as identifiers.
9362            TokenType::Identifier if tok.value == "select" || tok.value == "branch" => {
9363                self.parse_session_choice(&tok.value, loc)
9364            }
9365            // §Fase 79.b — `interrupt { <body> } on <Signal> as <sig> resumable { <handler> }`.
9366            // Contextual keyword (identifier), like `select`/`branch`.
9367            TokenType::Identifier if tok.value == "interrupt" => {
9368                self.parse_session_interrupt(loc)
9369            }
9370            // §Fase 79.b — `resume`: the handler's normal exit (hand control back to
9371            // the parked body). A bare step, no payload; only meaningful inside an
9372            // `interrupt` handler (enforced at type-check, §79.c).
9373            TokenType::Identifier if tok.value == "resume" => {
9374                self.advance();
9375                Ok(SessionStep { op: "resume".into(), loc, ..Default::default() })
9376            }
9377            _ => Err(ParseError {
9378                message: format!(
9379                    "Invalid session step '{}' — expected send | receive | loop | end | select | branch | interrupt | resume",
9380                    tok.value
9381                ),
9382                line: tok.line,
9383                column: tok.column,
9384                ..Default::default()
9385            }),
9386        }
9387    }
9388
9389    /// §Fase 79.b — consume a **contextual keyword** (`on` / `as` / `resumable`):
9390    /// a token whose *value* must equal `kw`, regardless of whether the lexer
9391    /// classified it as a keyword or a bare identifier. Keeps the `interrupt`
9392    /// surface readable without minting three reserved words.
9393    fn consume_contextual(&mut self, kw: &str) -> Result<(), ParseError> {
9394        let t = self.current().clone();
9395        if t.value != kw {
9396            return Err(ParseError {
9397                message: format!("expected `{kw}` in interrupt step, got `{}`", t.value),
9398                line: t.line,
9399                column: t.column,
9400                ..Default::default()
9401            });
9402        }
9403        self.advance();
9404        Ok(())
9405    }
9406
9407    /// §Fase 79.b — Parse an interruptible region:
9408    /// `interrupt { <body-steps> } on <Signal> as <sig> resumable { <handler-steps> }`.
9409    ///
9410    /// Encoded into the string-tagged `SessionStep` (mirroring the §41.b choice
9411    /// shape): `op = "interrupt"`, `message_type = <Signal>` (validated against the
9412    /// closed `CallInterruptCause` catalog at type-check, §79.c), two labelled
9413    /// `branches` (`body`, `handler`), `binder = <sig>`, `resumable = true`.
9414    fn parse_session_interrupt(&mut self, loc: Loc) -> Result<SessionStep, ParseError> {
9415        self.advance(); // consume `interrupt`
9416        // Body region — a brace-delimited step block (the paper's `interrupt { … }`
9417        // surface; distinct from the `[ … ]` step-lists of roles/choice arms).
9418        let body = self.parse_session_step_block()?;
9419        // `on <Signal>`
9420        self.consume_contextual("on")?;
9421        let signal = self.consume_any_ident_or_kw()?;
9422        // `as <sig>`
9423        self.consume_contextual("as")?;
9424        let binder = self.consume_any_ident_or_kw()?;
9425        // `resumable { <handler> }`
9426        self.consume_contextual("resumable")?;
9427        let handler = self.parse_session_step_block()?;
9428        Ok(SessionStep {
9429            op: "interrupt".into(),
9430            message_type: signal.value,
9431            branches: vec![
9432                SessionBranch { label: "body".into(), steps: body, loc: loc.clone() },
9433                SessionBranch { label: "handler".into(), steps: handler, loc: loc.clone() },
9434            ],
9435            binder: binder.value,
9436            resumable: true,
9437            loc,
9438        })
9439    }
9440
9441    /// §Fase 41.b — Parse a choice step: `select { ask: [..], cancel: [..] }`
9442    /// (or `branch { … }`). Each `label: [steps]` arm is a nested sub-protocol.
9443    fn parse_session_choice(&mut self, op: &str, loc: Loc) -> Result<SessionStep, ParseError> {
9444        self.advance(); // consume `select` / `branch`
9445        self.consume(TokenType::LBrace)?;
9446        let mut branches = Vec::new();
9447        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9448            let label_tok = self.consume_any_ident_or_kw()?;
9449            self.consume(TokenType::Colon)?;
9450            let steps = self.parse_session_steps()?;
9451            branches.push(SessionBranch {
9452                label: label_tok.value,
9453                steps,
9454                loc: Loc { line: label_tok.line, column: label_tok.column },
9455            });
9456            if self.check(TokenType::Comma) {
9457                self.advance();
9458            }
9459        }
9460        self.consume(TokenType::RBrace)?;
9461        Ok(SessionStep { op: op.to_string(), branches, loc, ..Default::default() })
9462    }
9463
9464    /// Parse: `topology Name { nodes: [A, B, …]  edges: [A -> B : Session, …] }`.
9465    fn parse_topology(&mut self) -> Result<TopologyDefinition, ParseError> {
9466        let tok = self.consume(TokenType::Topology)?;
9467        let name = self.consume(TokenType::Identifier)?.value;
9468        let mut node = TopologyDefinition {
9469            name,
9470            nodes: Vec::new(),
9471            edges: Vec::new(),
9472            loc: Loc {
9473                line: tok.line,
9474                column: tok.column,
9475            },
9476            leading_trivia: Vec::new(),
9477            trailing_trivia: Vec::new(),
9478        };
9479        self.consume(TokenType::LBrace)?;
9480        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9481            let field_name = self.current().value.clone();
9482            self.advance();
9483            if !self.check(TokenType::Colon) {
9484                if self.check(TokenType::LBrace) {
9485                    self.skip_braced_block()?;
9486                }
9487                continue;
9488            }
9489            self.advance();
9490            match field_name.as_str() {
9491                "nodes" => node.nodes = self.parse_bracketed_identifiers()?,
9492                "edges" => node.edges = self.parse_topology_edges()?,
9493                _ => self.skip_value(),
9494            }
9495        }
9496        self.consume(TokenType::RBrace)?;
9497        Ok(node)
9498    }
9499
9500    fn parse_topology_edges(&mut self) -> Result<Vec<TopologyEdge>, ParseError> {
9501        self.consume(TokenType::LBracket)?;
9502        let mut edges = Vec::new();
9503        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
9504            edges.push(self.parse_topology_edge()?);
9505            if self.check(TokenType::Comma) {
9506                self.advance();
9507            }
9508        }
9509        self.consume(TokenType::RBracket)?;
9510        Ok(edges)
9511    }
9512
9513    fn parse_topology_edge(&mut self) -> Result<TopologyEdge, ParseError> {
9514        let src_tok = self.consume_any_ident_or_kw()?;
9515        self.consume(TokenType::Arrow)?;
9516        let tgt_tok = self.consume_any_ident_or_kw()?;
9517        self.consume(TokenType::Colon)?;
9518        let sess_tok = self.consume_any_ident_or_kw()?;
9519        Ok(TopologyEdge {
9520            source: src_tok.value,
9521            target: tgt_tok.value,
9522            session_ref: sess_tok.value,
9523            loc: Loc {
9524                line: src_tok.line,
9525                column: src_tok.column,
9526            },
9527        })
9528    }
9529
9530    // ── §λ-L-E Fase 5 — Cognitive immune system (paper_immune_v2.md) ────
9531
9532    /// Parse: `immune Name { watch, sensitivity, baseline, window, scope, tau, decay }`.
9533    fn parse_immune(&mut self) -> Result<ImmuneDefinition, ParseError> {
9534        let tok = self.consume(TokenType::Immune)?;
9535        let name = self.consume(TokenType::Identifier)?.value;
9536        let mut node = ImmuneDefinition {
9537            name,
9538            watch: Vec::new(),
9539            sensitivity: None,
9540            baseline: "learned".to_string(),
9541            window: 100,
9542            scope: String::new(),
9543            tau: String::new(),
9544            decay: "exponential".to_string(),
9545            loc: Loc {
9546                line: tok.line,
9547                column: tok.column,
9548            },
9549            leading_trivia: Vec::new(),
9550            trailing_trivia: Vec::new(),
9551        };
9552        self.consume(TokenType::LBrace)?;
9553        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9554            let field_name = self.current().value.clone();
9555            self.advance();
9556            if !self.check(TokenType::Colon) {
9557                if self.check(TokenType::LBrace) {
9558                    self.skip_braced_block()?;
9559                }
9560                continue;
9561            }
9562            self.advance();
9563            match field_name.as_str() {
9564                "watch" => node.watch = self.parse_bracketed_identifiers()?,
9565                "sensitivity" => node.sensitivity = self.parse_optional_float(),
9566                "baseline" => node.baseline = self.consume_any_ident_or_kw()?.value,
9567                "window" => {
9568                    if let Some(v) = self.parse_optional_int() {
9569                        node.window = v;
9570                    }
9571                }
9572                "scope" => {
9573                    let s_tok = self.consume_any_ident_or_kw()?;
9574                    let s = s_tok.value;
9575                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
9576                        return Err(ParseError {
9577                            message: format!(
9578                                "Invalid scope '{s}' in immune '{}' — \
9579                                 expected tenant | flow | global",
9580                                node.name
9581                            ),
9582                            line: s_tok.line,
9583                            column: s_tok.column,
9584                                                    ..Default::default()
9585                        });
9586                    }
9587                    node.scope = s;
9588                }
9589                "tau" => {
9590                    let t = self.current().clone();
9591                    match t.ttype {
9592                        TokenType::Duration | TokenType::StringLit => {
9593                            self.advance();
9594                            node.tau = t.value;
9595                        }
9596                        _ => node.tau = self.consume_any_ident_or_kw()?.value,
9597                    }
9598                }
9599                "decay" => {
9600                    let d_tok = self.consume_any_ident_or_kw()?;
9601                    let d = d_tok.value;
9602                    if !matches!(d.as_str(), "exponential" | "linear" | "none") {
9603                        return Err(ParseError {
9604                            message: format!(
9605                                "Invalid decay '{d}' in immune '{}' — \
9606                                 expected exponential | linear | none",
9607                                node.name
9608                            ),
9609                            line: d_tok.line,
9610                            column: d_tok.column,
9611                                                    ..Default::default()
9612                        });
9613                    }
9614                    node.decay = d;
9615                }
9616                _ => self.skip_value(),
9617            }
9618        }
9619        self.consume(TokenType::RBrace)?;
9620        Ok(node)
9621    }
9622
9623    /// Parse: `reflex Name { trigger, on_level, action, scope, sla }`.
9624    fn parse_reflex(&mut self) -> Result<ReflexDefinition, ParseError> {
9625        let tok = self.consume(TokenType::Reflex)?;
9626        let name = self.consume(TokenType::Identifier)?.value;
9627        let mut node = ReflexDefinition {
9628            name,
9629            trigger: String::new(),
9630            on_level: "doubt".to_string(),
9631            action: String::new(),
9632            scope: String::new(),
9633            sla: String::new(),
9634            loc: Loc {
9635                line: tok.line,
9636                column: tok.column,
9637            },
9638            leading_trivia: Vec::new(),
9639            trailing_trivia: Vec::new(),
9640        };
9641        self.consume(TokenType::LBrace)?;
9642        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9643            let field_name = self.current().value.clone();
9644            self.advance();
9645            if !self.check(TokenType::Colon) {
9646                if self.check(TokenType::LBrace) {
9647                    self.skip_braced_block()?;
9648                }
9649                continue;
9650            }
9651            self.advance();
9652            match field_name.as_str() {
9653                "trigger" => node.trigger = self.consume_any_ident_or_kw()?.value,
9654                "on_level" => {
9655                    let l_tok = self.consume_any_ident_or_kw()?;
9656                    let l = l_tok.value;
9657                    if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
9658                        return Err(ParseError {
9659                            message: format!(
9660                                "Invalid on_level '{l}' in reflex '{}' — \
9661                                 expected know | believe | speculate | doubt",
9662                                node.name
9663                            ),
9664                            line: l_tok.line,
9665                            column: l_tok.column,
9666                                                    ..Default::default()
9667                        });
9668                    }
9669                    node.on_level = l;
9670                }
9671                "action" => {
9672                    let a_tok = self.consume_any_ident_or_kw()?;
9673                    let a = a_tok.value;
9674                    if !matches!(
9675                        a.as_str(),
9676                        "drop"
9677                            | "revoke"
9678                            | "emit"
9679                            | "redact"
9680                            | "quarantine"
9681                            | "terminate"
9682                            | "alert"
9683                    ) {
9684                        return Err(ParseError {
9685                            message: format!(
9686                                "Invalid action '{a}' in reflex '{}' — \
9687                                 expected drop | revoke | emit | redact | \
9688                                 quarantine | terminate | alert",
9689                                node.name
9690                            ),
9691                            line: a_tok.line,
9692                            column: a_tok.column,
9693                                                    ..Default::default()
9694                        });
9695                    }
9696                    node.action = a;
9697                }
9698                "scope" => {
9699                    let s_tok = self.consume_any_ident_or_kw()?;
9700                    let s = s_tok.value;
9701                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
9702                        return Err(ParseError {
9703                            message: format!(
9704                                "Invalid scope '{s}' in reflex '{}' — \
9705                                 expected tenant | flow | global",
9706                                node.name
9707                            ),
9708                            line: s_tok.line,
9709                            column: s_tok.column,
9710                                                    ..Default::default()
9711                        });
9712                    }
9713                    node.scope = s;
9714                }
9715                "sla" => {
9716                    let t = self.current().clone();
9717                    match t.ttype {
9718                        TokenType::Duration | TokenType::StringLit => {
9719                            self.advance();
9720                            node.sla = t.value;
9721                        }
9722                        _ => node.sla = self.consume_any_ident_or_kw()?.value,
9723                    }
9724                }
9725                _ => self.skip_value(),
9726            }
9727        }
9728        self.consume(TokenType::RBrace)?;
9729        Ok(node)
9730    }
9731
9732    /// Parse: `heal Name { source, on_level, mode, scope, review_sla, shield, max_patches }`.
9733    fn parse_heal(&mut self) -> Result<HealDefinition, ParseError> {
9734        let tok = self.consume(TokenType::Heal)?;
9735        let name = self.consume(TokenType::Identifier)?.value;
9736        let mut node = HealDefinition {
9737            name,
9738            source: String::new(),
9739            on_level: "doubt".to_string(),
9740            mode: "human_in_loop".to_string(),
9741            scope: String::new(),
9742            review_sla: String::new(),
9743            shield_ref: String::new(),
9744            max_patches: 3,
9745            loc: Loc {
9746                line: tok.line,
9747                column: tok.column,
9748            },
9749            leading_trivia: Vec::new(),
9750            trailing_trivia: Vec::new(),
9751        };
9752        self.consume(TokenType::LBrace)?;
9753        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9754            let field_name = self.current().value.clone();
9755            self.advance();
9756            if !self.check(TokenType::Colon) {
9757                if self.check(TokenType::LBrace) {
9758                    self.skip_braced_block()?;
9759                }
9760                continue;
9761            }
9762            self.advance();
9763            match field_name.as_str() {
9764                "source" => node.source = self.consume_any_ident_or_kw()?.value,
9765                "on_level" => {
9766                    let l_tok = self.consume_any_ident_or_kw()?;
9767                    let l = l_tok.value;
9768                    if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
9769                        return Err(ParseError {
9770                            message: format!(
9771                                "Invalid on_level '{l}' in heal '{}' — \
9772                                 expected know | believe | speculate | doubt",
9773                                node.name
9774                            ),
9775                            line: l_tok.line,
9776                            column: l_tok.column,
9777                                                    ..Default::default()
9778                        });
9779                    }
9780                    node.on_level = l;
9781                }
9782                "mode" => {
9783                    let m_tok = self.consume_any_ident_or_kw()?;
9784                    let m = m_tok.value;
9785                    if !matches!(m.as_str(), "audit_only" | "human_in_loop" | "adversarial") {
9786                        return Err(ParseError {
9787                            message: format!(
9788                                "Invalid mode '{m}' in heal '{}' — \
9789                                 expected audit_only | human_in_loop | adversarial",
9790                                node.name
9791                            ),
9792                            line: m_tok.line,
9793                            column: m_tok.column,
9794                                                    ..Default::default()
9795                        });
9796                    }
9797                    node.mode = m;
9798                }
9799                "scope" => {
9800                    let s_tok = self.consume_any_ident_or_kw()?;
9801                    let s = s_tok.value;
9802                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
9803                        return Err(ParseError {
9804                            message: format!(
9805                                "Invalid scope '{s}' in heal '{}' — \
9806                                 expected tenant | flow | global",
9807                                node.name
9808                            ),
9809                            line: s_tok.line,
9810                            column: s_tok.column,
9811                                                    ..Default::default()
9812                        });
9813                    }
9814                    node.scope = s;
9815                }
9816                "review_sla" => {
9817                    let t = self.current().clone();
9818                    match t.ttype {
9819                        TokenType::Duration | TokenType::StringLit => {
9820                            self.advance();
9821                            node.review_sla = t.value;
9822                        }
9823                        _ => node.review_sla = self.consume_any_ident_or_kw()?.value,
9824                    }
9825                }
9826                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
9827                "max_patches" => {
9828                    if let Some(v) = self.parse_optional_int() {
9829                        node.max_patches = v;
9830                    }
9831                }
9832                _ => self.skip_value(),
9833            }
9834        }
9835        self.consume(TokenType::RBrace)?;
9836        Ok(node)
9837    }
9838
9839    // ── §λ-L-E Fase 9 — UI cognitiva (component / view) ────────────
9840
9841    /// Parse: `component Name { renders, via_shield, on_interact, render_hint }`.
9842    fn parse_component(&mut self) -> Result<ComponentDefinition, ParseError> {
9843        let tok = self.consume(TokenType::Component)?;
9844        let name = self.consume(TokenType::Identifier)?.value;
9845        let mut node = ComponentDefinition {
9846            name,
9847            renders: String::new(),
9848            via_shield: String::new(),
9849            on_interact: String::new(),
9850            render_hint: "custom".to_string(),
9851            loc: Loc {
9852                line: tok.line,
9853                column: tok.column,
9854            },
9855            leading_trivia: Vec::new(),
9856            trailing_trivia: Vec::new(),
9857        };
9858        self.consume(TokenType::LBrace)?;
9859        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9860            let field_name = self.current().value.clone();
9861            self.advance();
9862            if !self.check(TokenType::Colon) {
9863                if self.check(TokenType::LBrace) {
9864                    self.skip_braced_block()?;
9865                }
9866                continue;
9867            }
9868            self.advance();
9869            match field_name.as_str() {
9870                "renders" => node.renders = self.consume_any_ident_or_kw()?.value,
9871                "via_shield" => node.via_shield = self.consume_any_ident_or_kw()?.value,
9872                "on_interact" => node.on_interact = self.consume_any_ident_or_kw()?.value,
9873                "render_hint" => {
9874                    let h_tok = self.consume_any_ident_or_kw()?;
9875                    let h = h_tok.value;
9876                    if !matches!(h.as_str(), "card" | "list" | "form" | "chart" | "custom") {
9877                        return Err(ParseError {
9878                            message: format!(
9879                                "Invalid render_hint '{h}' in component '{}' — \
9880                                 expected card | list | form | chart | custom",
9881                                node.name
9882                            ),
9883                            line: h_tok.line,
9884                            column: h_tok.column,
9885                                                    ..Default::default()
9886                        });
9887                    }
9888                    node.render_hint = h;
9889                }
9890                _ => self.skip_value(),
9891            }
9892        }
9893        self.consume(TokenType::RBrace)?;
9894        Ok(node)
9895    }
9896
9897    /// Parse: `view Name { title, components: [...], route }`.
9898    fn parse_view(&mut self) -> Result<ViewDefinition, ParseError> {
9899        let tok = self.consume(TokenType::View)?;
9900        let name = self.consume(TokenType::Identifier)?.value;
9901        let mut node = ViewDefinition {
9902            name,
9903            title: String::new(),
9904            components: Vec::new(),
9905            route: String::new(),
9906            loc: Loc {
9907                line: tok.line,
9908                column: tok.column,
9909            },
9910            leading_trivia: Vec::new(),
9911            trailing_trivia: Vec::new(),
9912        };
9913        self.consume(TokenType::LBrace)?;
9914        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9915            let field_name = self.current().value.clone();
9916            self.advance();
9917            if !self.check(TokenType::Colon) {
9918                if self.check(TokenType::LBrace) {
9919                    self.skip_braced_block()?;
9920                }
9921                continue;
9922            }
9923            self.advance();
9924            match field_name.as_str() {
9925                "title" => node.title = self.consume(TokenType::StringLit)?.value,
9926                "components" => node.components = self.parse_bracketed_identifiers()?,
9927                "route" => node.route = self.consume(TokenType::StringLit)?.value,
9928                _ => self.skip_value(),
9929            }
9930        }
9931        self.consume(TokenType::RBrace)?;
9932        Ok(node)
9933    }
9934
9935    fn parse_axonendpoint(&mut self) -> Result<AxonEndpointDefinition, ParseError> {
9936        let tok = self.consume(TokenType::AxonEndpoint)?;
9937        let name = self.consume(TokenType::Identifier)?.value;
9938        let mut node = AxonEndpointDefinition {
9939            name,
9940            method: String::new(),
9941            path: String::new(),
9942            body_type: String::new(),
9943            execute_flow: String::new(),
9944            output_type: String::new(),
9945            shield_ref: String::new(),
9946            // §Fase 83.a — `cors:` reference; empty ≡ no cors declared
9947            // (D83.5: no CORS headers, ever — secure by default).
9948            cors_ref: String::new(),
9949            retries: None,
9950            timeout: String::new(),
9951            compliance: Vec::new(),
9952            // §Fase 30 — Defaults preserve backwards compat per D1.
9953            transport: "json".to_string(),
9954            keepalive: String::new(),
9955            // §Fase 31.b — Inference fields (parser-default state).
9956            // Both fields toggle/populate only when the source provides
9957            // an explicit `transport:` declaration (parser sets
9958            // `transport_explicit = true`) AND the type-checker walks
9959            // the program to compute `implicit_transport`.
9960            transport_explicit: false,
9961            implicit_transport: String::new(),
9962            // §Fase 32.g (D8) — auth scope; empty list ≡ no auth gate.
9963            requires_capabilities: Vec::new(),
9964            // §Fase 89.a — explicit authorization-coverage opt-out. Default
9965            // false; the §89.b rule requires coverage OR `public: true`.
9966            public: false,
9967            // §Fase 32.h — Replay-token binding (D9 plan-vivo).
9968            // Parser defaults: not explicit; effective value resolved
9969            // at deploy time using the method-default heuristic.
9970            replay_explicit: false,
9971            replay: false,
9972            // §Fase 33.z.k.b (v1.28.0) — Wire-format dialect default
9973            // empty; the runtime classifier resolves the default
9974            // dialect per the algebraic-effect predicate when the
9975            // source omits `transport: sse(<dialect>)`.
9976            transport_dialect: String::new(),
9977            // §Fase 33.z.k.1 (v1.27.1) — Algebraic-effect override.
9978            // Parser default false; populated by the type-checker's
9979            // compute_implicit_transports pass once the full program
9980            // is known (the predicate cross-references tool effects
9981            // declared anywhere in the program).
9982            has_algebraic_stream_effect: false,
9983            // §Fase 36.d (D2) — declared execution backend; empty ≡
9984            // not declared (the endpoint resolves down the Fase 36 D1
9985            // ladder). A non-empty value is validated against the
9986            // closed `AXONENDPOINT_BACKEND_VALUES` catalog below.
9987            backend: String::new(),
9988            // §Fase 37.y (D1) — Path-param names extracted from the
9989            // `path:` string AFTER the field is parsed. Initialized
9990            // empty; populated by `extract_path_param_names` after
9991            // the `path:` field is read in the loop below.
9992            path_params: Vec::new(),
9993            // §Fase 37.y (D2) — Inline `query: { name: Type, name: Type? }`
9994            // block. Initialized empty; populated by the `"query"` arm
9995            // in the field loop below. Closed catalog enforced at parse
9996            // time per `axonendpoint_is_valid_query_param_type`.
9997            query_params: Vec::new(),
9998            loc: Loc {
9999                line: tok.line,
10000                column: tok.column,
10001            },
10002            leading_trivia: Vec::new(),
10003            trailing_trivia: Vec::new(),
10004        };
10005        self.consume(TokenType::LBrace)?;
10006        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10007            let field_name = self.current().value.clone();
10008            self.advance();
10009            if self.check(TokenType::Colon) {
10010                self.advance();
10011                match field_name.as_str() {
10012                    "method" => {
10013                        // §Fase 32.b D3 — closed method enum
10014                        // `{GET, POST, PUT, DELETE, PATCH}`. Unknown
10015                        // values rejected at parse time with smart-
10016                        // suggest hint (Fase 28.e). HEAD/OPTIONS/etc.
10017                        // are runtime-managed and not adopter-
10018                        // declarable.
10019                        let value_tok = self.consume_any_ident_or_kw()?;
10020                        let value_upper = value_tok.value.to_uppercase();
10021                        if !axonendpoint_is_valid_method(&value_upper) {
10022                            let hint = crate::smart_suggest::suggest_for(
10023                                &value_upper,
10024                                AXONENDPOINT_METHOD_VALUES,
10025                            );
10026                            let base = format!(
10027                                "Invalid method '{}' in axonendpoint '{}'.",
10028                                value_tok.value, node.name
10029                            );
10030                            let message = if hint.is_empty() {
10031                                format!(
10032                                    "{base} expected GET | POST | PUT | DELETE | PATCH, found {}",
10033                                    value_tok.value
10034                                )
10035                            } else {
10036                                format!(
10037                                    "{base} {hint} (expected GET | POST | PUT | DELETE | PATCH, found {})",
10038                                    value_tok.value
10039                                )
10040                            };
10041                            return Err(ParseError {
10042                                message,
10043                                line: value_tok.line,
10044                                column: value_tok.column,
10045                                ..Default::default()
10046                            });
10047                        }
10048                        node.method = value_upper;
10049                    }
10050                    "path" => {
10051                        node.path = self.consume(TokenType::StringLit)?.value.clone();
10052                        // §Fase 37.y (D1) — extract `{name}` placeholders
10053                        // for the Request Binding Contract's path-param
10054                        // source. Duplicate `{name}` in the same path
10055                        // is rejected at parse time (HTTP route patterns
10056                        // structurally reject duplicates; surfacing the
10057                        // error here is friendlier than letting axum
10058                        // panic at registration).
10059                        match extract_path_param_names(&node.path) {
10060                            Ok(names) => node.path_params = names,
10061                            Err(dup) => {
10062                                let cur = self.current().clone();
10063                                return Err(ParseError {
10064                                    message: format!(
10065                                        "axonendpoint '{}' declares path '{}' \
10066                                         containing duplicate placeholder '{{{}}}'. \
10067                                         Each `{{name}}` in a `path:` must be \
10068                                         unique — the runtime cannot bind two \
10069                                         path segments to the same name (Fase 37.y D1).",
10070                                        node.name, node.path, dup,
10071                                    ),
10072                                    line: cur.line,
10073                                    column: cur.column,
10074                                    ..Default::default()
10075                                });
10076                            }
10077                        }
10078                    },
10079                    "body" => node.body_type = self.consume_any_ident_or_kw()?.value.clone(),
10080                    "query" => {
10081                        // §Fase 37.y (D2) — Inline query-parameter block.
10082                        // Grammar: `query: { name: Type [, name: Type?]* }`.
10083                        // Closed type catalog
10084                        // `AXONENDPOINT_QUERY_PARAM_TYPES = {Text, Int,
10085                        // Float, Bool, Uuid}`. Optional via `?` suffix
10086                        // reuses `TypeExpr.optional` semantics already in
10087                        // use for flow parameters + body type fields. A
10088                        // duplicate field name in the same block is a
10089                        // parse error (HTTP query strings DO allow
10090                        // multi-value but v1.38.5 binds the first value
10091                        // only — see plan vivo §7 forward-compat).
10092                        //
10093                        // §Fase 37.y (D2 robustness) — declaring `query:`
10094                        // twice on the same axonendpoint silently merged
10095                        // params pre-hardening. Now it's a parse error
10096                        // so an adopter typo / copy-paste mistake
10097                        // surfaces with line + column instead of
10098                        // producing an unexpectedly-augmented endpoint.
10099                        let lbrace_tok = self.consume(TokenType::LBrace)?;
10100                        let block_line = lbrace_tok.line;
10101                        if !node.query_params.is_empty() {
10102                            return Err(ParseError {
10103                                message: format!(
10104                                    "axonendpoint '{}' declares `query: {{ … }}` \
10105                                     more than once. The query-parameter block \
10106                                     is unique per endpoint; combine all params \
10107                                     into a single block (Fase 37.y D2).",
10108                                    node.name,
10109                                ),
10110                                line: lbrace_tok.line,
10111                                column: lbrace_tok.column,
10112                                ..Default::default()
10113                            });
10114                        }
10115                        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10116                            let name_tok = self.consume(TokenType::Identifier)?;
10117                            let field_name = name_tok.value.clone();
10118                            // Duplicate detection within the block.
10119                            if node
10120                                .query_params
10121                                .iter()
10122                                .any(|f| f.name == field_name)
10123                            {
10124                                return Err(ParseError {
10125                                    message: format!(
10126                                        "axonendpoint '{}' declares duplicate \
10127                                         query param '{}' inside `query: {{ … }}`. \
10128                                         Each name must appear at most once \
10129                                         (Fase 37.y D2).",
10130                                        node.name, field_name,
10131                                    ),
10132                                    line: name_tok.line,
10133                                    column: name_tok.column,
10134                                    ..Default::default()
10135                                });
10136                            }
10137                            self.consume(TokenType::Colon)?;
10138                            let type_expr = self.parse_type_expr()?;
10139                            // §Fase 37.y (D2 robustness) — reject generic
10140                            // type expressions on query params. The
10141                            // closed catalog is 5 primitives; container
10142                            // types (`Optional<T>`, `List<T>`, etc.)
10143                            // would mislead the adopter into thinking
10144                            // they bind multi-value query strings
10145                            // (deferred per plan vivo §7) or that
10146                            // `Optional<Text>` is the canonical way to
10147                            // declare an optional query (it's NOT —
10148                            // `Text?` is). Surface the canonical syntax
10149                            // verbatim so the fix is obvious.
10150                            if !type_expr.generic_param.is_empty() {
10151                                let canonical_hint = if type_expr.name == "Optional" {
10152                                    format!(
10153                                        " Use `{}?` (the `?` suffix) for an \
10154                                         optional query param instead of \
10155                                         `Optional<{}>`.",
10156                                        type_expr.generic_param,
10157                                        type_expr.generic_param,
10158                                    )
10159                                } else if type_expr.name == "List" {
10160                                    " Multi-value query params (e.g. `?tag=a&tag=b`) \
10161                                     are honest-deferred from v1.38.5; bind a \
10162                                     single-value `Text` query param and parse \
10163                                     the value inside the flow."
10164                                        .to_string()
10165                                } else {
10166                                    String::new()
10167                                };
10168                                return Err(ParseError {
10169                                    message: format!(
10170                                        "axonendpoint '{}' query param '{}' uses \
10171                                         a generic type `{}<{}>`. Query params \
10172                                         take a primitive type from the closed \
10173                                         catalog ({}); the `?` suffix marks \
10174                                         optional.{} (Fase 37.y D2).",
10175                                        node.name,
10176                                        field_name,
10177                                        type_expr.name,
10178                                        type_expr.generic_param,
10179                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | "),
10180                                        canonical_hint,
10181                                    ),
10182                                    line: type_expr.loc.line,
10183                                    column: type_expr.loc.column,
10184                                    ..Default::default()
10185                                });
10186                            }
10187                            // Validate against the closed catalog. A
10188                            // miss surfaces a Fase 28-style smart-suggest
10189                            // hint when within edit-distance 2.
10190                            if !axonendpoint_is_valid_query_param_type(&type_expr.name) {
10191                                // `smart_suggest::suggest_for` returns
10192                                // pre-formatted prose like
10193                                // "Did you mean `Text`?" or
10194                                // "Did you mean `Text` or `Int`?" (empty
10195                                // when no candidate within edit-distance
10196                                // 2). Concatenate without re-wrapping.
10197                                let hint = crate::smart_suggest::suggest_for(
10198                                    &type_expr.name,
10199                                    AXONENDPOINT_QUERY_PARAM_TYPES,
10200                                );
10201                                let hint_text = if hint.is_empty() {
10202                                    format!(
10203                                        " Expected one of: {}.",
10204                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | ")
10205                                    )
10206                                } else {
10207                                    format!(
10208                                        " {} Expected one of: {}.",
10209                                        hint,
10210                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | ")
10211                                    )
10212                                };
10213                                return Err(ParseError {
10214                                    message: format!(
10215                                        "axonendpoint '{}' query param '{}' has \
10216                                         unsupported type '{}'.{} (Fase 37.y D2).",
10217                                        node.name, field_name, type_expr.name,
10218                                        hint_text,
10219                                    ),
10220                                    line: type_expr.loc.line,
10221                                    column: type_expr.loc.column,
10222                                    ..Default::default()
10223                                });
10224                            }
10225                            node.query_params.push(TypeField {
10226                                name: field_name,
10227                                type_expr,
10228                                loc: Loc {
10229                                    line: name_tok.line,
10230                                    column: name_tok.column,
10231                                },
10232                            });
10233                            // Trailing comma is optional; the next loop
10234                            // iteration handles `}` cleanly. Accept both
10235                            // `name: Type, name: Type` AND `name: Type
10236                            // name: Type` (the existing parser style is
10237                            // forgiving about list separators).
10238                            if self.check(TokenType::Comma) {
10239                                self.advance();
10240                            }
10241                            let _ = block_line; // suppress unused warning
10242                        }
10243                        self.consume(TokenType::RBrace)?;
10244                    },
10245                    "execute" => node.execute_flow = self.consume_any_ident_or_kw()?.value.clone(),
10246                    "output" => {
10247                        // §Fase 38.x.f — promote axonendpoint `output:`
10248                        // parsing from a single token to the full
10249                        // generic-aware type expression (mirroring
10250                        // `parse_step` for FlowStep::Step which already
10251                        // uses `parse_output_type_string`).
10252                        //
10253                        // Pre-38.x.f: `output: List<Item>` captured only
10254                        // `"List"`, dropping `<Item>` (next tokens were
10255                        // either left unconsumed or absorbed by the
10256                        // following field). v1.39.0's narrow cardinality
10257                        // gate happened to fire correctly for `output: T`
10258                        // + retrieve-tail because the singular-detection
10259                        // path used `!starts_with("List<")` — but the
10260                        // SYMMETRIC `output: List<T>` + singular-tail
10261                        // case (38.x.f D3) needs the FULL `List<T>`
10262                        // shape captured; without it the gate sees
10263                        // `"List"` and misclassifies as Singular.
10264                        node.output_type = self.parse_output_type_string()?;
10265                    }
10266                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
10267                    // §Fase 83.a — the `cors: <Name>` reference.
10268                    "cors" => node.cors_ref = self.consume_any_ident_or_kw()?.value.clone(),
10269                    "retries" => node.retries = self.parse_optional_int(),
10270                    "timeout" => {
10271                        let t = self.current().clone();
10272                        self.advance();
10273                        node.timeout = t.value.clone();
10274                    }
10275                    "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
10276                    "replay" => {
10277                        // §Fase 32.h (D9 plan-vivo) — Replay-token binding.
10278                        // Boolean `replay: true | false`. Default (when
10279                        // omitted) is method-derived at deploy-time:
10280                        // POST/PUT → true, GET/DELETE → false. Explicit
10281                        // declaration sets `replay_explicit = true` so
10282                        // the runtime knows NOT to override.
10283                        let value_tok = self.consume(TokenType::Bool)?;
10284                        node.replay = value_tok.value.eq_ignore_ascii_case("true");
10285                        node.replay_explicit = true;
10286                    }
10287                    // §Fase 89.a — `public: true | false`, the explicit
10288                    // authorization-coverage opt-out (doctrine
10289                    // `every_boundary_is_guarded`). Mirrors `replay:`'s bool
10290                    // parse. Default false; the §89.b rule (`axon-T890`)
10291                    // requires a covering discipline OR `public: true`.
10292                    "public" => {
10293                        let value_tok = self.consume(TokenType::Bool)?;
10294                        node.public = value_tok.value.eq_ignore_ascii_case("true");
10295                    }
10296                    "requires" => {
10297                        // §Fase 32.g (D8) — Auth scope per axonendpoint.
10298                        // Closed slug grammar
10299                        // `^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$` enforced
10300                        // at parse time with smart-suggest-style hint.
10301                        // Empty list means "no auth gate" (D9 backwards-
10302                        // compat). Cross-stack with Python parser.
10303                        let bracket_tok = self.current().clone();
10304                        let items = self.parse_bracketed_dot_identifiers()?;
10305                        for slug in &items {
10306                            if !is_valid_capability_slug(slug) {
10307                                return Err(ParseError {
10308                                    message: format!(
10309                                        "Invalid capability slug '{slug}' in axonendpoint '{}' \
10310                                         `requires:`. Capability slugs must match \
10311                                         ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
10312                                         lowercase identifiers starting with a letter. Examples: \
10313                                         `admin`, `legal.read`, `hipaa.phi.read`.",
10314                                        node.name
10315                                    ),
10316                                    line: bracket_tok.line,
10317                                    column: bracket_tok.column,
10318                                    ..Default::default()
10319                                });
10320                            }
10321                        }
10322                        node.requires_capabilities = items;
10323                    }
10324                    // §Fase 30.b — HTTP transport enum (D2 closed) + keepalive (D6 closed).
10325                    // Mirrors `axon/compiler/parser.py` `_parse_axonendpoint`.
10326                    // Drift-gate corpus verifies byte-identical parse cross-stack.
10327                    "transport" => {
10328                        let value_tok = self.consume_any_ident_or_kw()?;
10329                        let value = &value_tok.value;
10330                        if !axonendpoint_is_valid_transport(value) {
10331                            let hint = crate::smart_suggest::suggest_for(
10332                                value,
10333                                AXONENDPOINT_TRANSPORT_VALUES,
10334                            );
10335                            let base = format!(
10336                                "Invalid transport '{}' in axonendpoint '{}'.",
10337                                value, node.name
10338                            );
10339                            let message = if hint.is_empty() {
10340                                format!("{base} expected json | sse | ndjson, found {value}")
10341                            } else {
10342                                format!(
10343                                    "{base} {hint} (expected json | sse | ndjson, found {value})"
10344                                )
10345                            };
10346                            return Err(ParseError {
10347                                message,
10348                                line: value_tok.line,
10349                                column: value_tok.column,
10350                                ..Default::default()
10351                            });
10352                        }
10353                        node.transport = value.clone();
10354                        // §Fase 31.b D1 — mark the field as explicitly
10355                        // declared so the type-checker's implicit-transport
10356                        // inference knows NOT to override this value with
10357                        // the produces_stream-driven inference.
10358                        node.transport_explicit = true;
10359                        // §Fase 33.z.k.b (v1.28.0) — Optional dialect
10360                        // parametrization: `transport: sse(<dialect>)`.
10361                        // Only valid when the base value is `sse`
10362                        // (json + ndjson dialects are the dialects
10363                        // themselves; `json(<x>)` / `ndjson(<x>)`
10364                        // would be parse errors caught below).
10365                        if self.check(TokenType::LParen) {
10366                            if value != "sse" {
10367                                let tok = self.current().clone();
10368                                return Err(ParseError {
10369                                    message: format!(
10370                                        "Dialect parametrization \
10371                                         `transport: {value}(<dialect>)` is \
10372                                         only valid for `sse`; got \
10373                                         `{value}` in axonendpoint '{}'.",
10374                                        node.name
10375                                    ),
10376                                    line: tok.line,
10377                                    column: tok.column,
10378                                    ..Default::default()
10379                                });
10380                            }
10381                            self.advance(); // consume LParen
10382                            let dialect_tok = self.consume_any_ident_or_kw()?;
10383                            let dialect = dialect_tok.value.clone();
10384                            if !AXONENDPOINT_TRANSPORT_DIALECTS
10385                                .iter()
10386                                .any(|&d| d == dialect)
10387                            {
10388                                let hint = crate::smart_suggest::suggest_for(
10389                                    &dialect,
10390                                    AXONENDPOINT_TRANSPORT_DIALECTS,
10391                                );
10392                                let base = format!(
10393                                    "Invalid SSE dialect '{dialect}' in axonendpoint '{}'.",
10394                                    node.name
10395                                );
10396                                let message = if hint.is_empty() {
10397                                    format!(
10398                                        "{base} expected axon | openai | kimi | glm | anthropic, found {dialect}"
10399                                    )
10400                                } else {
10401                                    format!(
10402                                        "{base} {hint} (expected axon | openai | kimi | glm | anthropic, found {dialect})"
10403                                    )
10404                                };
10405                                return Err(ParseError {
10406                                    message,
10407                                    line: dialect_tok.line,
10408                                    column: dialect_tok.column,
10409                                    ..Default::default()
10410                                });
10411                            }
10412                            // Closing RParen.
10413                            let rparen_tok = self.current().clone();
10414                            if !self.check(TokenType::RParen) {
10415                                return Err(ParseError {
10416                                    message: format!(
10417                                        "Expected `)` after dialect name \
10418                                         in axonendpoint '{}' \
10419                                         (transport: sse(<dialect>) grammar).",
10420                                        node.name
10421                                    ),
10422                                    line: rparen_tok.line,
10423                                    column: rparen_tok.column,
10424                                    ..Default::default()
10425                                });
10426                            }
10427                            self.advance(); // consume RParen
10428                            node.transport_dialect = dialect;
10429                        }
10430                    }
10431                    "keepalive" => {
10432                        // Accepts either a DURATION token (e.g. `15s`) or
10433                        // an ident-like token. Validation against the
10434                        // closed enum {5s, 15s, 30s, 60s} happens after.
10435                        let value_tok = self.current().clone();
10436                        self.advance();
10437                        let value = &value_tok.value;
10438                        if !axonendpoint_is_valid_keepalive(value) {
10439                            let hint = crate::smart_suggest::suggest_for(
10440                                value,
10441                                AXONENDPOINT_KEEPALIVE_VALUES,
10442                            );
10443                            let base = format!(
10444                                "Invalid keepalive '{}' in axonendpoint '{}'.",
10445                                value, node.name
10446                            );
10447                            let message = if hint.is_empty() {
10448                                format!("{base} expected 5s | 15s | 30s | 60s, found {value}")
10449                            } else {
10450                                format!(
10451                                    "{base} {hint} (expected 5s | 15s | 30s | 60s, found {value})"
10452                                )
10453                            };
10454                            return Err(ParseError {
10455                                message,
10456                                line: value_tok.line,
10457                                column: value_tok.column,
10458                                ..Default::default()
10459                            });
10460                        }
10461                        node.keepalive = value.clone();
10462                    }
10463                    "backend" => {
10464                        // §Fase 36.d (D2) — declared execution backend.
10465                        // Closed catalog `CANONICAL_PROVIDERS ∪ {auto,
10466                        // stub}`; an unknown name is a parse error with
10467                        // a smart-suggest hint (the same discipline as
10468                        // `method`/`transport`/`keepalive`). The
10469                        // type-checker re-validates defensively for
10470                        // ASTs built outside the parser (LSP, tests).
10471                        let value_tok = self.consume_any_ident_or_kw()?;
10472                        let value = &value_tok.value;
10473                        if !axonendpoint_is_valid_backend(value) {
10474                            let hint = crate::smart_suggest::suggest_for(
10475                                value,
10476                                AXONENDPOINT_BACKEND_VALUES,
10477                            );
10478                            let expected = AXONENDPOINT_BACKEND_VALUES.join(" | ");
10479                            let base = format!(
10480                                "Invalid backend '{}' in axonendpoint '{}'.",
10481                                value, node.name
10482                            );
10483                            let message = if hint.is_empty() {
10484                                format!("{base} expected {expected}, found {value}")
10485                            } else {
10486                                format!(
10487                                    "{base} {hint} (expected {expected}, found {value})"
10488                                )
10489                            };
10490                            return Err(ParseError {
10491                                message,
10492                                line: value_tok.line,
10493                                column: value_tok.column,
10494                                ..Default::default()
10495                            });
10496                        }
10497                        node.backend = value.clone();
10498                    }
10499                    _ => self.skip_value(),
10500                }
10501            } else if self.check(TokenType::LBrace) {
10502                self.skip_braced_block()?;
10503            }
10504        }
10505        self.consume(TokenType::RBrace)?;
10506        Ok(node)
10507    }
10508
10509    // ── Numeric helpers for Tier 2 field parsing ────────────────────
10510
10511    fn parse_optional_int(&mut self) -> Option<i64> {
10512        let tok = self.current().clone();
10513        match tok.ttype {
10514            TokenType::Integer => {
10515                self.advance();
10516                tok.value.parse::<i64>().ok()
10517            }
10518            _ => {
10519                self.advance();
10520                None
10521            }
10522        }
10523    }
10524
10525    fn parse_optional_float(&mut self) -> Option<f64> {
10526        let tok = self.current().clone();
10527        match tok.ttype {
10528            TokenType::Float | TokenType::Integer => {
10529                self.advance();
10530                tok.value.parse::<f64>().ok()
10531            }
10532            _ => {
10533                self.advance();
10534                None
10535            }
10536        }
10537    }
10538
10539    // ── LAMBDA DATA (ΛD) ──────────────────────────────────────────
10540
10541    fn parse_lambda_data(&mut self) -> Result<LambdaDataDefinition, ParseError> {
10542        let tok = self.consume(TokenType::Lambda)?;
10543        let name = self.consume(TokenType::Identifier)?;
10544        self.consume(TokenType::LBrace)?;
10545
10546        let mut node = LambdaDataDefinition {
10547            name: name.value.clone(),
10548            ontology: String::new(),
10549            certainty: 1.0,
10550            temporal_frame_start: String::new(),
10551            temporal_frame_end: String::new(),
10552            provenance: String::new(),
10553            derivation: String::new(),
10554            loc: Loc {
10555                line: tok.line,
10556                column: tok.column,
10557            },
10558            leading_trivia: Vec::new(),
10559            trailing_trivia: Vec::new(),
10560        };
10561
10562        while !self.check(TokenType::RBrace) {
10563            let field = self.current().clone();
10564            match field.ttype {
10565                TokenType::Ontology => {
10566                    self.advance();
10567                    self.consume(TokenType::Colon)?;
10568                    node.ontology = self.consume(TokenType::StringLit)?.value.clone();
10569                }
10570                TokenType::Certainty => {
10571                    self.advance();
10572                    self.consume(TokenType::Colon)?;
10573                    let val = self.current().clone();
10574                    match val.ttype {
10575                        TokenType::Float => {
10576                            self.advance();
10577                            node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
10578                        }
10579                        TokenType::Integer => {
10580                            self.advance();
10581                            node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
10582                        }
10583                        _ => {
10584                            return Err(ParseError {
10585                                message: format!(
10586                                    "Expected number for certainty, got '{}'",
10587                                    val.value
10588                                ),
10589                                line: val.line,
10590                                column: val.column,
10591                                                            ..Default::default()
10592                            });
10593                        }
10594                    }
10595                }
10596                TokenType::TemporalFrame => {
10597                    self.advance();
10598                    self.consume(TokenType::Colon)?;
10599                    node.temporal_frame_start = self.consume(TokenType::StringLit)?.value.clone();
10600                    // Optional second string for end frame
10601                    if self.check(TokenType::StringLit) {
10602                        node.temporal_frame_end = self.consume(TokenType::StringLit)?.value.clone();
10603                    }
10604                }
10605                TokenType::Provenance => {
10606                    self.advance();
10607                    self.consume(TokenType::Colon)?;
10608                    node.provenance = self.consume(TokenType::StringLit)?.value.clone();
10609                }
10610                TokenType::Derivation => {
10611                    self.advance();
10612                    self.consume(TokenType::Colon)?;
10613                    let d = self.current().clone();
10614                    self.advance();
10615                    node.derivation = d.value.clone();
10616                }
10617                _ => {
10618                    // Skip unknown fields gracefully
10619                    self.advance();
10620                    if self.check(TokenType::Colon) {
10621                        self.advance();
10622                        self.skip_value();
10623                    }
10624                }
10625            }
10626        }
10627
10628        self.consume(TokenType::RBrace)?;
10629        Ok(node)
10630    }
10631
10632    fn parse_lambda_data_apply(&mut self) -> Result<LambdaDataApplyNode, ParseError> {
10633        let tok = self.consume(TokenType::Lambda)?;
10634        let lambda_name = self.consume(TokenType::Identifier)?;
10635
10636        // Expect "on" keyword (parsed as identifier since it's not reserved)
10637        let on_tok = self.current().clone();
10638        self.advance();
10639        if on_tok.value != "on" {
10640            return Err(ParseError {
10641                message: format!(
10642                    "Expected 'on' after lambda data name in flow step, got '{}'",
10643                    on_tok.value
10644                ),
10645                line: on_tok.line,
10646                column: on_tok.column,
10647                            ..Default::default()
10648            });
10649        }
10650
10651        let target = self.current().clone();
10652        self.advance();
10653
10654        let mut output_type = String::new();
10655        if self.check(TokenType::Arrow) {
10656            self.advance();
10657            output_type = self.consume(TokenType::Identifier)?.value.clone();
10658        }
10659
10660        Ok(LambdaDataApplyNode {
10661            lambda_data_name: lambda_name.value.clone(),
10662            target: target.value.clone(),
10663            output_type,
10664            loc: Loc {
10665                line: tok.line,
10666                column: tok.column,
10667            },
10668        })
10669    }
10670
10671    // ── GENERIC (Tier 2+) ────────────────────────────────────────
10672
10673    fn parse_generic_declaration(&mut self) -> Result<Declaration, ParseError> {
10674        let kw_tok = self.current().clone();
10675        self.advance(); // consume keyword
10676
10677        // Try to consume a name (identifier or keyword-as-name)
10678        let name = if self.current().ttype == TokenType::Identifier {
10679            let n = self.current().value.clone();
10680            self.advance();
10681            n
10682        } else if !self.check(TokenType::LBrace)
10683            && !self.check(TokenType::LParen)
10684            && !self.check(TokenType::Eof)
10685            && self
10686                .current()
10687                .value
10688                .chars()
10689                .all(|c| c.is_alphanumeric() || c == '_')
10690        {
10691            let n = self.current().value.clone();
10692            self.advance();
10693            n
10694        } else {
10695            String::new()
10696        };
10697
10698        // Skip optional parens: (...)
10699        if self.check(TokenType::LParen) {
10700            self.advance();
10701            let mut depth = 1u32;
10702            while depth > 0 && !self.check(TokenType::Eof) {
10703                if self.check(TokenType::LParen) {
10704                    depth += 1;
10705                } else if self.check(TokenType::RParen) {
10706                    depth -= 1;
10707                }
10708                self.advance();
10709            }
10710        }
10711
10712        // Skip tokens until LBrace or next declaration
10713        while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
10714            if self.check(TokenType::Eof) {
10715                break;
10716            }
10717            self.advance();
10718        }
10719
10720        // Skip braced block if present
10721        if self.check(TokenType::LBrace) {
10722            self.skip_braced_block()?;
10723        }
10724
10725        Ok(Declaration::Generic(GenericDeclaration {
10726            keyword: kw_tok.value,
10727            name,
10728            loc: Loc {
10729                line: kw_tok.line,
10730                column: kw_tok.column,
10731            },
10732            leading_trivia: Vec::new(),
10733            trailing_trivia: Vec::new(),
10734        }))
10735    }
10736
10737    // ──────────────────────────────────────────────────────────────────
10738    //  §λ-L-E Fase 13 — Mobile Typed Channels parsers
10739    //  (paper_mobile_channels.md §3 + plan/fase_13)
10740    //  Direct port of axon/compiler/parser.py:_parse_channel/emit/publish/discover.
10741    // ──────────────────────────────────────────────────────────────────
10742
10743    /// Parse: `channel Name { message, qos, lifetime, persistence, shield }`.
10744    fn parse_channel(&mut self) -> Result<ChannelDefinition, ParseError> {
10745        let tok = self.consume(TokenType::Channel)?;
10746        let name = self.consume(TokenType::Identifier)?.value;
10747        let mut node = ChannelDefinition {
10748            name: name.clone(),
10749            message: String::new(),
10750            qos: "at_least_once".to_string(),
10751            lifetime: "affine".to_string(),
10752            persistence: "ephemeral".to_string(),
10753            shield_ref: String::new(),
10754            loc: Loc {
10755                line: tok.line,
10756                column: tok.column,
10757            },
10758            leading_trivia: Vec::new(),
10759            trailing_trivia: Vec::new(),
10760        };
10761        self.consume(TokenType::LBrace)?;
10762        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10763            let field_tok = self.current().clone();
10764            let field_name = field_tok.value.clone();
10765            self.advance();
10766            if !self.check(TokenType::Colon) {
10767                if self.check(TokenType::LBrace) {
10768                    self.skip_braced_block()?;
10769                }
10770                continue;
10771            }
10772            self.advance();
10773            match field_name.as_str() {
10774                "message" => node.message = self.parse_channel_message_type()?,
10775                "qos" => {
10776                    let q_tok = self.consume_any_ident_or_kw()?;
10777                    if !matches!(
10778                        q_tok.value.as_str(),
10779                        "at_most_once" | "at_least_once" | "exactly_once" | "broadcast" | "queue"
10780                    ) {
10781                        return Err(ParseError {
10782                            message: format!(
10783                                "Invalid qos '{}' in channel '{}' — \
10784                                 expected at_most_once | at_least_once | \
10785                                 exactly_once | broadcast | queue",
10786                                q_tok.value, name
10787                            ),
10788                            line: q_tok.line,
10789                            column: q_tok.column,
10790                                                    ..Default::default()
10791                        });
10792                    }
10793                    node.qos = q_tok.value;
10794                }
10795                "lifetime" => {
10796                    let lt_tok = self.consume_any_ident_or_kw()?;
10797                    if !matches!(lt_tok.value.as_str(), "linear" | "affine" | "persistent") {
10798                        return Err(ParseError {
10799                            message: format!(
10800                                "Invalid lifetime '{}' in channel '{}' — \
10801                                 expected linear | affine | persistent",
10802                                lt_tok.value, name
10803                            ),
10804                            line: lt_tok.line,
10805                            column: lt_tok.column,
10806                                                    ..Default::default()
10807                        });
10808                    }
10809                    node.lifetime = lt_tok.value;
10810                }
10811                "persistence" => {
10812                    let p_tok = self.consume_any_ident_or_kw()?;
10813                    if !matches!(p_tok.value.as_str(), "ephemeral" | "persistent_axonstore") {
10814                        return Err(ParseError {
10815                            message: format!(
10816                                "Invalid persistence '{}' in channel '{}' — \
10817                                 expected ephemeral | persistent_axonstore",
10818                                p_tok.value, name
10819                            ),
10820                            line: p_tok.line,
10821                            column: p_tok.column,
10822                                                    ..Default::default()
10823                        });
10824                    }
10825                    node.persistence = p_tok.value;
10826                }
10827                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
10828                _ => self.skip_value(),
10829            }
10830        }
10831        self.consume(TokenType::RBrace)?;
10832        Ok(node)
10833    }
10834
10835    /// Parse a `message:` value, supporting nested `Channel<…>`
10836    /// (second-order session types — paper §3.3).
10837    fn parse_channel_message_type(&mut self) -> Result<String, ParseError> {
10838        let head = self.consume(TokenType::Identifier)?;
10839        let mut spelling = head.value;
10840        if self.check(TokenType::Lt) {
10841            self.advance();
10842            let inner = self.parse_channel_message_type()?;
10843            self.consume(TokenType::Gt)?;
10844            spelling = format!("{}<{}>", spelling, inner);
10845        }
10846        Ok(spelling)
10847    }
10848
10849    /// Parse: `emit ChannelName(value_ref)` — Chan-Output / Chan-Mobility.
10850    ///
10851    /// `value_ref` accepts a bare identifier (variable / channel name for
10852    /// mobility) or a dotted path (`Step.output.field`) referencing a prior
10853    /// step result (Fase 13.i — runtime resolves via ContextManager).
10854    fn parse_emit_step(&mut self) -> Result<FlowStep, ParseError> {
10855        let tok = self.consume(TokenType::Emit)?;
10856        let channel = self.consume(TokenType::Identifier)?.value;
10857        self.consume(TokenType::LParen)?;
10858        let value = self.parse_emit_value_ref()?;
10859        self.consume(TokenType::RParen)?;
10860        Ok(FlowStep::Emit(EmitStatement {
10861            channel_ref: channel,
10862            value_ref: value,
10863            loc: Loc {
10864                line: tok.line,
10865                column: tok.column,
10866            },
10867        }))
10868    }
10869
10870    /// §Fase 92.b — parse `mint <Credential> as <binding>`. The credential
10871    /// reference must resolve to a declared `credential` (`axon-T895`,
10872    /// type-checker); the binding is a fresh flow-scoped name receiving the
10873    /// raw bearer string. Both tokens are required — a `mint` with no
10874    /// binding would mint authority into the void.
10875    fn parse_mint_step(&mut self) -> Result<FlowStep, ParseError> {
10876        let tok = self.consume(TokenType::Mint)?;
10877        let credential_ref = self.consume(TokenType::Identifier)?.value;
10878        self.consume(TokenType::As)?;
10879        let binding = self.consume(TokenType::Identifier)?.value;
10880        Ok(FlowStep::Mint(MintStep {
10881            credential_ref,
10882            binding,
10883            loc: Loc {
10884                line: tok.line,
10885                column: tok.column,
10886            },
10887        }))
10888    }
10889
10890    /// §Fase 94.b — parse `rotate <SecretsStore> [where "<filter>"] with
10891    /// <Tool> as <binding>` (doctrine `rotation_without_revelation`).
10892    ///
10893    /// All three anchors are grammar, not convention: the store names WHAT
10894    /// may rotate (a `backend: secrets` class view — `axon-T898` in the
10895    /// type-checker), the tool names WHO performs the exchange
10896    /// (`axon-T899`), and the binding receives the metadata-only summary —
10897    /// a `rotate` without a binding would renew authority with no
10898    /// observable outcome, so `as` is REQUIRED (the `mint` posture). The
10899    /// `where` filter is optional (§67 string grammar, proven against the
10900    /// synthesized metadata schema); omitting it rotates the WHOLE class —
10901    /// the deliberate post-breach bulk shape. `with` is a soft keyword
10902    /// (not a lexer token): reserving it globally would break every
10903    /// adopter identifier named `with`.
10904    fn parse_rotate_step(&mut self) -> Result<FlowStep, ParseError> {
10905        let tok = self.consume(TokenType::Rotate)?;
10906        let store_ref = self.consume(TokenType::Identifier)?.value;
10907        let mut where_expr = String::new();
10908        if self.check(TokenType::Where) {
10909            self.advance();
10910            where_expr = self.consume(TokenType::StringLit)?.value.clone();
10911        }
10912        let with_tok = self.current().clone();
10913        if with_tok.value != "with" {
10914            return Err(ParseError {
10915                message: format!(
10916                    "Expected `with <Tool>` after `rotate {store_ref}{}`, found '{}'. \
10917                     A rotation names the tool that performs the renewal exchange: \
10918                     `rotate {store_ref} [where \"<filter>\"] with <Tool> as <binding>`.",
10919                    if where_expr.is_empty() { "" } else { " where …" },
10920                    with_tok.value
10921                ),
10922                line: with_tok.line,
10923                column: with_tok.column,
10924                ..Default::default()
10925            });
10926        }
10927        self.advance();
10928        let tool_ref = self.consume(TokenType::Identifier)?.value;
10929        self.consume(TokenType::As)?;
10930        let binding = self.consume(TokenType::Identifier)?.value;
10931        Ok(FlowStep::Rotate(RotateStep {
10932            store_ref,
10933            where_expr,
10934            tool_ref,
10935            binding,
10936            loc: Loc {
10937                line: tok.line,
10938                column: tok.column,
10939            },
10940        }))
10941    }
10942
10943    /// Parse: `IDENTIFIER ('.' (IDENTIFIER | keyword))*` → dot-joined string
10944    /// (Fase 13.i).
10945    ///
10946    /// Mirrors the Python `_parse_emit_value_ref` helper exactly so the IR
10947    /// JSON for `emit Hello(Build.output)` is byte-identical between the
10948    /// two reference implementations.
10949    ///
10950    /// The HEAD must be a real ``Identifier``. Subsequent segments after a
10951    /// `.` may be identifiers OR keywords — common field names like
10952    /// ``output``, ``result``, ``message``, ``state``, etc. are reserved
10953    /// words in Axon but adopters must be able to write them as
10954    /// dotted-access segments. The accepting predicate:
10955    ///   - the lexer carried a non-empty `value` (every Word-like token does)
10956    ///   - the value's first byte is a letter or underscore (filters out
10957    ///     punctuation tokens such as ',', '{', etc.)
10958    fn parse_emit_value_ref(&mut self) -> Result<String, ParseError> {
10959        let head = self.consume(TokenType::Identifier)?.value;
10960        let mut parts = vec![head];
10961        while self.check(TokenType::Dot) {
10962            self.advance(); // consume '.'
10963            let next_tok = self.current().clone();
10964            let valid = !next_tok.value.is_empty()
10965                && next_tok.value.as_bytes()[0].is_ascii_alphabetic()
10966                || next_tok.value.starts_with('_');
10967            if !valid {
10968                return Err(ParseError {
10969                    message: format!(
10970                        "Expected identifier or keyword after '.' in dotted \
10971                         access, found {:?}",
10972                        next_tok.value
10973                    ),
10974                    line: next_tok.line,
10975                    column: next_tok.column,
10976                                    ..Default::default()
10977                });
10978            }
10979            self.advance();
10980            parts.push(next_tok.value);
10981        }
10982        Ok(parts.join("."))
10983    }
10984
10985    /// Parse: `publish ChannelName within ShieldName` — Publish-Ext (D8).
10986    fn parse_publish_step(&mut self) -> Result<FlowStep, ParseError> {
10987        let tok = self.consume(TokenType::Publish)?;
10988        let channel = self.consume(TokenType::Identifier)?.value;
10989        self.consume(TokenType::Within)?;
10990        let shield = self.consume(TokenType::Identifier)?.value;
10991        Ok(FlowStep::Publish(PublishStatement {
10992            channel_ref: channel,
10993            shield_ref: shield,
10994            loc: Loc {
10995                line: tok.line,
10996                column: tok.column,
10997            },
10998        }))
10999    }
11000
11001    /// Parse: `discover ChannelName as alias` — dual of publish.
11002    fn parse_discover_step(&mut self) -> Result<FlowStep, ParseError> {
11003        let tok = self.consume(TokenType::Discover)?;
11004        let cap = self.consume(TokenType::Identifier)?.value;
11005        self.consume(TokenType::As)?;
11006        let alias = self.consume(TokenType::Identifier)?.value;
11007        Ok(FlowStep::Discover(DiscoverStatement {
11008            capability_ref: cap,
11009            alias,
11010            loc: Loc {
11011                line: tok.line,
11012                column: tok.column,
11013            },
11014        }))
11015    }
11016}
11017
11018// ── §λ-L-E Fase 13 — Mobile Typed Channels parser tests ─────────────────────
11019
11020#[cfg(test)]
11021mod fase13_parser_tests {
11022    use super::*;
11023    use crate::lexer::Lexer;
11024
11025    fn parse(src: &str) -> Result<Program, ParseError> {
11026        let tokens = Lexer::new(src, "<test>").tokenize().expect("lex");
11027        Parser::new(tokens).parse()
11028    }
11029
11030    #[test]
11031    fn channel_full_parses() {
11032        let src = r#"channel C { message: Order qos: at_least_once lifetime: affine persistence: ephemeral shield: Gate }"#;
11033        let prog = parse(src).expect("parse");
11034        match &prog.declarations[0] {
11035            Declaration::Channel(c) => {
11036                assert_eq!(c.name, "C");
11037                assert_eq!(c.message, "Order");
11038                assert_eq!(c.qos, "at_least_once");
11039                assert_eq!(c.lifetime, "affine");
11040                assert_eq!(c.persistence, "ephemeral");
11041                assert_eq!(c.shield_ref, "Gate");
11042            }
11043            _ => panic!("expected ChannelDefinition"),
11044        }
11045    }
11046
11047    #[test]
11048    fn channel_defaults_match_paper_d1() {
11049        let prog = parse("channel C { message: Order }").expect("parse");
11050        if let Declaration::Channel(c) = &prog.declarations[0] {
11051            assert_eq!(c.qos, "at_least_once"); // default
11052            assert_eq!(c.lifetime, "affine"); // D1 default
11053            assert_eq!(c.persistence, "ephemeral");
11054            assert_eq!(c.shield_ref, "");
11055        } else {
11056            panic!("expected ChannelDefinition");
11057        }
11058    }
11059
11060    #[test]
11061    fn channel_second_order_message_type_parses() {
11062        let prog = parse("channel C { message: Channel<Order> }").expect("parse");
11063        if let Declaration::Channel(c) = &prog.declarations[0] {
11064            assert_eq!(c.message, "Channel<Order>");
11065        } else {
11066            panic!("expected ChannelDefinition");
11067        }
11068    }
11069
11070    #[test]
11071    fn channel_nested_channel_message_type_parses() {
11072        let prog = parse("channel C { message: Channel<Channel<Order>> }").expect("parse");
11073        if let Declaration::Channel(c) = &prog.declarations[0] {
11074            assert_eq!(c.message, "Channel<Channel<Order>>");
11075        } else {
11076            panic!("expected ChannelDefinition");
11077        }
11078    }
11079
11080    #[test]
11081    fn channel_invalid_qos_rejected() {
11082        let err = parse("channel C { message: T qos: bogus }").unwrap_err();
11083        assert!(err.message.contains("Invalid qos"), "got {}", err.message);
11084    }
11085
11086    #[test]
11087    fn channel_invalid_lifetime_rejected() {
11088        let err = parse("channel C { message: T lifetime: eternal }").unwrap_err();
11089        assert!(
11090            err.message.contains("Invalid lifetime"),
11091            "got {}",
11092            err.message
11093        );
11094    }
11095
11096    #[test]
11097    fn channel_invalid_persistence_rejected() {
11098        let err = parse("channel C { message: T persistence: forever }").unwrap_err();
11099        assert!(
11100            err.message.contains("Invalid persistence"),
11101            "got {}",
11102            err.message
11103        );
11104    }
11105
11106    #[test]
11107    fn emit_value_parses() {
11108        let src = "flow f() -> Out { emit C(payload) }";
11109        let prog = parse(src).expect("parse");
11110        if let Declaration::Flow(f) = &prog.declarations[0] {
11111            match &f.body[0] {
11112                FlowStep::Emit(e) => {
11113                    assert_eq!(e.channel_ref, "C");
11114                    assert_eq!(e.value_ref, "payload");
11115                }
11116                other => panic!("expected Emit, got {:?}", other),
11117            }
11118        } else {
11119            panic!("expected Flow");
11120        }
11121    }
11122
11123    #[test]
11124    fn publish_within_shield_parses() {
11125        let src = "flow f() -> Cap { publish C within Gate }";
11126        let prog = parse(src).expect("parse");
11127        if let Declaration::Flow(f) = &prog.declarations[0] {
11128            match &f.body[0] {
11129                FlowStep::Publish(p) => {
11130                    assert_eq!(p.channel_ref, "C");
11131                    assert_eq!(p.shield_ref, "Gate");
11132                }
11133                other => panic!("expected Publish, got {:?}", other),
11134            }
11135        } else {
11136            panic!("expected Flow");
11137        }
11138    }
11139
11140    #[test]
11141    fn discover_with_alias_parses() {
11142        let src = "flow f() -> Out { discover C as ch }";
11143        let prog = parse(src).expect("parse");
11144        if let Declaration::Flow(f) = &prog.declarations[0] {
11145            match &f.body[0] {
11146                FlowStep::Discover(d) => {
11147                    assert_eq!(d.capability_ref, "C");
11148                    assert_eq!(d.alias, "ch");
11149                }
11150                other => panic!("expected Discover, got {:?}", other),
11151            }
11152        } else {
11153            panic!("expected Flow");
11154        }
11155    }
11156
11157    #[test]
11158    fn listen_typed_ref_sets_flag_true() {
11159        let src = "daemon D() { goal: \"x\" listen C as ev { } }";
11160        let prog = parse(src).expect("parse");
11161        if let Declaration::Daemon(d) = &prog.declarations[0] {
11162            assert_eq!(d.listeners.len(), 1);
11163            assert_eq!(d.listeners[0].channel, "C");
11164            assert!(d.listeners[0].channel_is_ref, "typed ref ⇒ true");
11165        } else {
11166            panic!("expected Daemon");
11167        }
11168    }
11169
11170    #[test]
11171    fn listen_string_topic_legacy_flag_false() {
11172        let src = "daemon D() { goal: \"x\" listen \"orders\" as ev { } }";
11173        let prog = parse(src).expect("parse");
11174        if let Declaration::Daemon(d) = &prog.declarations[0] {
11175            assert_eq!(d.listeners.len(), 1);
11176            assert_eq!(d.listeners[0].channel, "orders");
11177            assert!(!d.listeners[0].channel_is_ref, "string topic ⇒ false");
11178        } else {
11179            panic!("expected Daemon");
11180        }
11181    }
11182
11183    // ── Fase 13.i — emit value_ref accepts dotted access ───────────
11184
11185    fn extract_first_emit(prog: &Program) -> &EmitStatement {
11186        if let Declaration::Flow(f) = &prog.declarations[0] {
11187            if let FlowStep::Emit(e) = &f.body[0] {
11188                return e;
11189            }
11190        }
11191        panic!("expected emit statement at flow body[0]");
11192    }
11193
11194    #[test]
11195    fn emit_accepts_bare_identifier_value_ref() {
11196        // Pre-13.i baseline — must keep working.
11197        let prog = parse("flow f() -> Out { emit Hello(payload) }").expect("parse");
11198        let emit = extract_first_emit(&prog);
11199        assert_eq!(emit.channel_ref, "Hello");
11200        assert_eq!(emit.value_ref, "payload");
11201    }
11202
11203    #[test]
11204    fn emit_accepts_two_segment_dotted_value_ref() {
11205        // The exact case adopters reported as broken before 13.i.
11206        let prog = parse("flow f() -> Out { emit Hello(Build.output) }").expect("parse");
11207        let emit = extract_first_emit(&prog);
11208        assert_eq!(emit.value_ref, "Build.output");
11209    }
11210
11211    #[test]
11212    fn emit_accepts_three_segment_nested_dotted_value_ref() {
11213        let prog = parse("flow f() -> Out { emit Score(Analyze.result.score) }").expect("parse");
11214        let emit = extract_first_emit(&prog);
11215        assert_eq!(emit.value_ref, "Analyze.result.score");
11216    }
11217
11218    #[test]
11219    fn emit_dotted_with_trailing_dot_fails() {
11220        // Trailing `.` must still error — every '.' demands an identifier.
11221        let result = parse("flow f() -> Out { emit Hello(Build.) }");
11222        assert!(result.is_err(), "expected parse error for trailing dot");
11223    }
11224}
11225
11226// ── §Fase 14.a — declaration_trivia parallel channel tests ──────────────────
11227
11228#[cfg(test)]
11229mod fase14a_declaration_trivia_tests {
11230    use super::*;
11231    use crate::lexer::Lexer;
11232    use crate::tokens::TriviaKind;
11233
11234    fn parse(src: &str) -> Program {
11235        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
11236        Parser::new(toks).parse().expect("parse")
11237    }
11238
11239    #[test]
11240    fn no_comments_means_empty_trivia_per_decl() {
11241        let prog = parse("flow F() -> Out { }");
11242        assert_eq!(prog.declarations.len(), 1);
11243        assert_eq!(prog.declaration_trivia.len(), 1);
11244        assert!(prog.declaration_trivia[0].leading.is_empty());
11245        assert!(prog.declaration_trivia[0].trailing.is_empty());
11246    }
11247
11248    #[test]
11249    fn doc_line_comment_attaches_as_leading() {
11250        let prog = parse("/// Documents F\nflow F() -> Out { }");
11251        let triv = &prog.declaration_trivia[0];
11252        assert_eq!(triv.leading.len(), 1);
11253        assert_eq!(triv.leading[0].kind, TriviaKind::DocLine);
11254        assert!(triv.leading[0].is_doc());
11255        assert_eq!(triv.leading[0].text, "/// Documents F");
11256    }
11257
11258    #[test]
11259    fn regular_line_comment_attaches_as_leading() {
11260        let prog = parse("// header\nflow F() -> Out { }");
11261        let triv = &prog.declaration_trivia[0];
11262        assert_eq!(triv.leading.len(), 1);
11263        assert_eq!(triv.leading[0].kind, TriviaKind::Line);
11264        assert!(!triv.leading[0].is_doc());
11265    }
11266
11267    #[test]
11268    fn block_doc_comment_attaches_as_leading() {
11269        let prog = parse("/** Doc block */\nflow F() -> Out { }");
11270        let triv = &prog.declaration_trivia[0];
11271        assert_eq!(triv.leading[0].kind, TriviaKind::DocBlock);
11272        assert!(triv.leading[0].is_doc());
11273    }
11274
11275    #[test]
11276    fn multiple_comments_collected_in_source_order() {
11277        let src = "/// First\n/// Second\nflow F() -> Out { }";
11278        let prog = parse(src);
11279        let triv = &prog.declaration_trivia[0];
11280        assert_eq!(triv.leading.len(), 2);
11281        assert_eq!(triv.leading[0].text, "/// First");
11282        assert_eq!(triv.leading[1].text, "/// Second");
11283    }
11284
11285    #[test]
11286    fn three_decls_each_get_own_leading() {
11287        let src = "/// for A\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }\n/// for C\nflow C() -> Out { }";
11288        let prog = parse(src);
11289        assert_eq!(prog.declarations.len(), 3);
11290        assert_eq!(prog.declaration_trivia.len(), 3);
11291        for (idx, name) in ["A", "B", "C"].iter().enumerate() {
11292            let triv = &prog.declaration_trivia[idx];
11293            assert_eq!(triv.leading.len(), 1);
11294            assert_eq!(triv.leading[0].text, format!("/// for {name}"));
11295        }
11296    }
11297
11298    #[test]
11299    fn trailing_comment_attaches_to_last_token_of_decl() {
11300        // Comment on the same line as the decl's closing brace.
11301        let prog = parse("flow F() -> Out { } // tail");
11302        let triv = &prog.declaration_trivia[0];
11303        assert_eq!(triv.trailing.len(), 1);
11304        assert_eq!(triv.trailing[0].text, "// tail");
11305    }
11306
11307    #[test]
11308    fn mixed_doc_and_regular_preserve_order_between_decls() {
11309        let src = "/// doc for A\nflow A() -> Out { }\n\n// header line\n/// doc for B\nflow B() -> Out { }";
11310        let prog = parse(src);
11311        assert_eq!(prog.declarations.len(), 2);
11312        // A: just the doc comment.
11313        assert_eq!(prog.declaration_trivia[0].leading.len(), 1);
11314        // B: header + doc, in source order.
11315        assert_eq!(prog.declaration_trivia[1].leading.len(), 2);
11316        assert_eq!(prog.declaration_trivia[1].leading[0].text, "// header line");
11317        assert_eq!(prog.declaration_trivia[1].leading[1].text, "/// doc for B");
11318    }
11319
11320    #[test]
11321    fn parser_unaffected_by_comments_in_grammar_path() {
11322        // The parser must accept comments interleaved between every
11323        // legal token without affecting the AST shape it produces.
11324        // This is the regression guard for "lossless lexing must not
11325        // change parsing semantics."
11326        let src =
11327            "// before flow\nflow /* between flow and name */ F() -> Out {\n  // body comment\n}";
11328        let prog = parse(src);
11329        assert_eq!(prog.declarations.len(), 1);
11330        if let Declaration::Flow(f) = &prog.declarations[0] {
11331            assert_eq!(f.name, "F");
11332        } else {
11333            panic!("expected Flow declaration");
11334        }
11335    }
11336}
11337
11338// ── §Fase 14.b — per-struct trivia fields tests ─────────────────────────────
11339//
11340// 14.b spreads `leading_trivia` / `trailing_trivia` into every Declaration
11341// variant struct (FlowDefinition, ChannelDefinition, PersonaDefinition, …).
11342// The Python AST already had this shape since 14.a; 14.b achieves Rust
11343// parity. The side-channel `Program.declaration_trivia` is preserved for
11344// backward compat — these tests verify the new direct access path.
11345
11346#[cfg(test)]
11347mod fase14b_per_struct_trivia_tests {
11348    use super::*;
11349    use crate::lexer::Lexer;
11350    use crate::tokens::TriviaKind;
11351
11352    fn parse(src: &str) -> Program {
11353        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
11354        Parser::new(toks).parse().expect("parse")
11355    }
11356
11357    #[test]
11358    fn flow_definition_carries_leading_trivia_directly() {
11359        let prog = parse("/// documents F\nflow F() -> Out { }");
11360        if let Declaration::Flow(f) = &prog.declarations[0] {
11361            assert_eq!(f.leading_trivia.len(), 1);
11362            assert_eq!(f.leading_trivia[0].kind, TriviaKind::DocLine);
11363            assert_eq!(f.leading_trivia[0].text, "/// documents F");
11364            assert!(f.trailing_trivia.is_empty());
11365        } else {
11366            panic!("expected Flow declaration");
11367        }
11368    }
11369
11370    #[test]
11371    fn flow_definition_carries_trailing_trivia_directly() {
11372        let prog = parse("flow F() -> Out { } // tail comment");
11373        if let Declaration::Flow(f) = &prog.declarations[0] {
11374            assert_eq!(f.trailing_trivia.len(), 1);
11375            assert_eq!(f.trailing_trivia[0].text, "// tail comment");
11376        } else {
11377            panic!("expected Flow declaration");
11378        }
11379    }
11380
11381    #[test]
11382    fn channel_definition_carries_trivia_directly() {
11383        // ChannelDefinition is a Tier-1 declaration; verify per-struct fields
11384        // populate just like FlowDefinition.
11385        let src = concat!(
11386            "/// inbound order events\n",
11387            "channel Orders {\n",
11388            "    message:     Order\n",
11389            "    qos:         at_least_once\n",
11390            "    lifetime:    affine\n",
11391            "    persistence: ephemeral\n",
11392            "    shield:      Broker\n",
11393            "}",
11394        );
11395        let prog = parse(src);
11396        if let Declaration::Channel(ch) = &prog.declarations[0] {
11397            assert_eq!(ch.leading_trivia.len(), 1);
11398            assert!(ch.leading_trivia[0].is_doc());
11399            assert_eq!(ch.leading_trivia[0].text, "/// inbound order events");
11400        } else {
11401            panic!("expected Channel declaration");
11402        }
11403    }
11404
11405    #[test]
11406    fn per_struct_fields_match_side_channel() {
11407        // 14.a side-channel and 14.b per-struct fields must hold identical
11408        // data — they are populated by the same parser pass.
11409        let src = "/// for A\n// header for B\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }";
11410        let prog = parse(src);
11411        for (idx, decl) in prog.declarations.iter().enumerate() {
11412            let side = &prog.declaration_trivia[idx];
11413            let (per_lead, per_trail) = match decl {
11414                Declaration::Flow(f) => (&f.leading_trivia, &f.trailing_trivia),
11415                _ => panic!("unexpected variant"),
11416            };
11417            assert_eq!(per_lead.len(), side.leading.len());
11418            assert_eq!(per_trail.len(), side.trailing.len());
11419            for (a, b) in per_lead.iter().zip(side.leading.iter()) {
11420                assert_eq!(a.text, b.text);
11421                assert_eq!(a.kind, b.kind);
11422            }
11423        }
11424    }
11425
11426    #[test]
11427    fn comment_free_program_yields_empty_per_struct_fields() {
11428        let prog = parse("flow F() -> Out { }");
11429        if let Declaration::Flow(f) = &prog.declarations[0] {
11430            assert!(f.leading_trivia.is_empty());
11431            assert!(f.trailing_trivia.is_empty());
11432        } else {
11433            panic!("expected Flow declaration");
11434        }
11435    }
11436}
11437
11438// ── §Fase 14.c — inner doc comments (//!, /*!) ──────────────────────────────
11439//
11440// Inner doc comments document the *enclosing* item rather than the next
11441// sibling. Today they flow through the trivia channel like any other
11442// comment; downstream consumers (axon doc, LSP) decide how to interpret
11443// `is_inner_doc()`. These tests verify the lexer→parser pipeline preserves
11444// the inner-doc discriminator end-to-end.
11445
11446#[cfg(test)]
11447mod fase14c_inner_doc_tests {
11448    use super::*;
11449    use crate::lexer::Lexer;
11450    use crate::tokens::TriviaKind;
11451
11452    fn parse(src: &str) -> Program {
11453        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
11454        Parser::new(toks).parse().expect("parse")
11455    }
11456
11457    #[test]
11458    fn inner_doc_line_reaches_leading_trivia() {
11459        let src = "//! file-level docs\nflow F() -> Out { }";
11460        let prog = parse(src);
11461        let triv = &prog.declaration_trivia[0];
11462        assert_eq!(triv.leading.len(), 1);
11463        assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocLine);
11464        assert!(triv.leading[0].is_doc());
11465        assert!(triv.leading[0].is_inner_doc());
11466        assert_eq!(triv.leading[0].text, "//! file-level docs");
11467        assert_eq!(triv.leading[0].stripped_text(), " file-level docs");
11468    }
11469
11470    #[test]
11471    fn inner_doc_block_reaches_leading_trivia() {
11472        let src = "/*! module-level docs */\nflow F() -> Out { }";
11473        let prog = parse(src);
11474        let triv = &prog.declaration_trivia[0];
11475        assert_eq!(triv.leading.len(), 1);
11476        assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocBlock);
11477        assert!(triv.leading[0].is_inner_doc());
11478        assert_eq!(triv.leading[0].stripped_text(), " module-level docs ");
11479    }
11480
11481    #[test]
11482    fn outer_and_inner_doc_can_coexist() {
11483        // File-level inner doc on top, then an outer doc for the
11484        // declaration. Both reach the trivia channel and remain
11485        // distinguishable via `is_inner_doc()`.
11486        let src = "//! file docs\n/// docs F\nflow F() -> Out { }";
11487        let prog = parse(src);
11488        let triv = &prog.declaration_trivia[0];
11489        assert_eq!(triv.leading.len(), 2);
11490        assert!(triv.leading[0].is_inner_doc());
11491        assert!(triv.leading[1].is_doc());
11492        assert!(!triv.leading[1].is_inner_doc());
11493    }
11494
11495    #[test]
11496    fn inner_doc_reaches_per_struct_fields() {
11497        // Same data must be visible via the per-struct fields (Fase 14.b).
11498        let src = "//! intro\nflow F() -> Out { }";
11499        let prog = parse(src);
11500        if let Declaration::Flow(f) = &prog.declarations[0] {
11501            assert_eq!(f.leading_trivia.len(), 1);
11502            assert!(f.leading_trivia[0].is_inner_doc());
11503        } else {
11504            panic!("expected Flow declaration");
11505        }
11506    }
11507}
11508
11509// ── §Fase 28.c — Parser error recovery test pack ─────────────────────────────
11510//
11511// Mirror of `tests/test_fase28_parser_recovery.py` (Python side, 28.b).
11512// The test classes here line up 1-1 with the Python ones so the cross-
11513// stack drift gate (28.i) can compare error-list shapes input-for-input.
11514//
11515// Test classes:
11516//   - backwards_compat: existing `parse()` API unchanged
11517//   - single_error_recovery: one bad decl → one error, rest parse OK
11518//   - multi_error_recovery: N independent errors → N entries
11519//   - sync_points: every top-level keyword resyncs correctly
11520//   - parse_result_api: `has_errors`, `is_clean`
11521//   - edge_cases: EOF mid-error, brace imbalance, only-bad-tokens
11522//   - robustness_fuzz: 1000 deterministic-seeded mutations never crash
11523//   - no_ghost_errors: single broken field produces exactly 1 error
11524//   - integration_with_colon_diagnostic: v1.19.4 hint preserved under
11525//     recovery mode
11526#[cfg(test)]
11527mod fase28_recovery_tests {
11528    use super::*;
11529    use crate::lexer::Lexer;
11530
11531    /// Lex a source and return tokens for the parser to consume.
11532    /// Mirrors the Python `_parse_recovery` helper.
11533    fn lex(src: &str) -> Vec<Token> {
11534        Lexer::new(src, "<test>").tokenize().expect("lex")
11535    }
11536
11537    /// Parse with recovery mode. Returns `(program, errors)` so call
11538    /// sites read like the Python helper.
11539    fn recover(src: &str) -> ParseResult {
11540        Parser::new(lex(src)).parse_with_recovery()
11541    }
11542
11543    /// Strict parse. Mirrors the Python `_parse_strict` helper.
11544    fn strict(src: &str) -> Result<Program, ParseError> {
11545        Parser::new(lex(src)).parse()
11546    }
11547
11548    // ── backwards_compat ─────────────────────────────────────────
11549
11550    #[test]
11551    fn strict_parse_unchanged_for_clean_source() {
11552        // The existing `parse()` API must continue to succeed
11553        // verbatim on every well-formed input — D9.
11554        let src = "intent I {}";
11555        let prog = strict(src).expect("clean parse");
11556        assert_eq!(prog.declarations.len(), 1);
11557    }
11558
11559    #[test]
11560    fn strict_parse_still_raises_on_first_error() {
11561        // D9 + D8: opt-in to recovery via `parse_with_recovery`;
11562        // strict mode must still bubble the first error.
11563        // (Using a parse-time error rather than a lex error — `@@@`
11564        // would be rejected by the lexer, which is out of scope.)
11565        let src = "flow F() { } not_a_keyword flow G() { }";
11566        let _ = strict(src).expect_err("must error fast in strict mode");
11567    }
11568
11569    #[test]
11570    fn recovery_clean_source_yields_no_errors() {
11571        let src = "flow F() { } flow G() { }";
11572        let pr = recover(src);
11573        assert!(pr.is_clean(), "errors: {:?}", pr.errors);
11574        assert_eq!(pr.program.declarations.len(), 2);
11575    }
11576
11577    // ── single_error_recovery ────────────────────────────────────
11578
11579    #[test]
11580    fn single_unknown_top_level_token_recovers() {
11581        // One garbage token at top level; rest must parse.
11582        let src = "garbage_token flow F() { } flow G() { }";
11583        let pr = recover(src);
11584        assert_eq!(pr.errors.len(), 1, "errors: {:?}", pr.errors);
11585        assert_eq!(pr.program.declarations.len(), 2);
11586    }
11587
11588    #[test]
11589    fn error_in_first_decl_does_not_block_second() {
11590        // `flow F` body refers to non-keyword `nope`; the error
11591        // recovery must skip to the next top-level keyword.
11592        let src = "flow F() { not_a_step nope } flow G() { }";
11593        let pr = recover(src);
11594        assert!(pr.has_errors(), "expected at least one error");
11595        // The second flow must be reachable.
11596        let names: Vec<&str> = pr
11597            .program
11598            .declarations
11599            .iter()
11600            .filter_map(|d| match d {
11601                Declaration::Flow(f) => Some(f.name.as_str()),
11602                _ => None,
11603            })
11604            .collect();
11605        assert!(names.contains(&"G"), "G not found among {names:?}");
11606    }
11607
11608    #[test]
11609    fn malformed_declaration_then_clean_intent_recovers() {
11610        let src = "flow @ () { } intent I {}";
11611        let pr = recover(src);
11612        assert!(pr.has_errors());
11613        let kinds: Vec<&str> = pr
11614            .program
11615            .declarations
11616            .iter()
11617            .map(|d| match d {
11618                Declaration::Intent(_) => "intent",
11619                Declaration::Flow(_) => "flow",
11620                _ => "other",
11621            })
11622            .collect();
11623        assert!(kinds.contains(&"intent"), "kinds: {kinds:?}");
11624    }
11625
11626    #[test]
11627    fn recovery_does_not_double_count_a_single_error() {
11628        // Regression for the "ghost error" pathology that surfaced
11629        // during 28.b dev: a nested-decl error must not also fire
11630        // an "Unexpected token at top level" from the outer loop.
11631        // The Rust grammar has stricter intra-flow requirements
11632        // than Python; the invariant we assert here is that the
11633        // outer loop emits zero "Unexpected token at top level"
11634        // errors after an inner step-shape error.
11635        let src = "flow F() { not_a_step }";
11636        let pr = recover(src);
11637        let outer_ghosts = pr
11638            .errors
11639            .iter()
11640            .filter(|e| e.message.contains("at top level"))
11641            .count();
11642        assert_eq!(outer_ghosts, 0, "ghost errors: {:?}", pr.errors);
11643    }
11644
11645    // ── multi_error_recovery ─────────────────────────────────────
11646
11647    #[test]
11648    fn three_independent_errors_yield_three_entries() {
11649        let src =
11650            "garbage1 flow F() { } garbage2 flow G() { } garbage3 flow H() { }";
11651        let pr = recover(src);
11652        assert_eq!(pr.errors.len(), 3, "errors: {:?}", pr.errors);
11653        assert_eq!(pr.program.declarations.len(), 3);
11654    }
11655
11656    #[test]
11657    fn all_errors_no_valid_declarations() {
11658        let src = "foo bar baz qux";
11659        let pr = recover(src);
11660        assert!(pr.has_errors());
11661        assert!(pr.program.declarations.is_empty());
11662    }
11663
11664    #[test]
11665    fn errors_recorded_in_source_order() {
11666        let src = "x flow A() { } y flow B() { } z flow C() { }";
11667        let pr = recover(src);
11668        assert_eq!(pr.errors.len(), 3);
11669        let lines: Vec<u32> = pr.errors.iter().map(|e| e.line).collect();
11670        // Same source-line means we compare by column ordering;
11671        // either way they must be non-decreasing.
11672        assert!(
11673            lines.windows(2).all(|w| w[0] <= w[1]),
11674            "errors out of order: {lines:?}"
11675        );
11676    }
11677
11678    // ── sync_points ──────────────────────────────────────────────
11679
11680    #[test]
11681    fn sync_to_flow_keyword() {
11682        let src = "garbage flow F() { }";
11683        let pr = recover(src);
11684        assert_eq!(pr.program.declarations.len(), 1);
11685    }
11686
11687    #[test]
11688    fn sync_to_intent_keyword() {
11689        let src = "garbage intent I {}";
11690        let pr = recover(src);
11691        assert_eq!(pr.program.declarations.len(), 1);
11692    }
11693
11694    #[test]
11695    fn sync_to_persona_keyword() {
11696        let src = "garbage persona P { name: \"P\" role: \"R\" }";
11697        let pr = recover(src);
11698        assert!(
11699            pr.program
11700                .declarations
11701                .iter()
11702                .any(|d| matches!(d, Declaration::Persona(_))),
11703            "persona not recovered: decls = {:?}",
11704            pr.program.declarations.len()
11705        );
11706    }
11707
11708    #[test]
11709    fn sync_to_run_keyword() {
11710        let src = "garbage run R { input: { user_message: \"hi\" } }";
11711        let pr = recover(src);
11712        // Either Run was parsed, or recovery still produced ≥1 err.
11713        assert!(pr.has_errors());
11714    }
11715
11716    // ── parse_result_api ─────────────────────────────────────────
11717
11718    #[test]
11719    fn parse_result_has_errors_and_is_clean_invert() {
11720        let pr_clean = recover("flow F() { }");
11721        assert!(pr_clean.is_clean());
11722        assert!(!pr_clean.has_errors());
11723
11724        let pr_err = recover("garbage");
11725        assert!(!pr_err.is_clean());
11726        assert!(pr_err.has_errors());
11727    }
11728
11729    #[test]
11730    fn parse_result_program_field_holds_partial_program() {
11731        let pr = recover("garbage flow F() { }");
11732        assert!(!pr.program.declarations.is_empty());
11733    }
11734
11735    #[test]
11736    fn parse_result_errors_carry_line_and_column() {
11737        let pr = recover("garbage");
11738        assert!(!pr.errors.is_empty());
11739        let e = &pr.errors[0];
11740        assert!(e.line >= 1);
11741        // Column may be 0-based or 1-based depending on lexer;
11742        // accept anything ≥ 0.
11743        let _ = e.column;
11744        assert!(!e.message.is_empty());
11745    }
11746
11747    #[test]
11748    fn parse_result_debug_renders() {
11749        let pr = recover("flow F() { }");
11750        let s = format!("{pr:?}");
11751        assert!(s.contains("ParseResult"));
11752    }
11753
11754    // ── edge_cases ───────────────────────────────────────────────
11755
11756    #[test]
11757    fn empty_source_is_clean() {
11758        let pr = recover("");
11759        assert!(pr.is_clean());
11760        assert!(pr.program.declarations.is_empty());
11761    }
11762
11763    #[test]
11764    fn whitespace_only_source_is_clean() {
11765        let pr = recover("   \n\n\t  \n");
11766        assert!(pr.is_clean());
11767        assert!(pr.program.declarations.is_empty());
11768    }
11769
11770    #[test]
11771    fn only_garbage_does_not_crash() {
11772        // Lex-clean garbage tokens (avoids AxonLexerError).
11773        let pr = recover("foo bar baz { qux quux } corge { grault }");
11774        assert!(pr.has_errors());
11775    }
11776
11777    #[test]
11778    fn unbalanced_close_brace_does_not_crash() {
11779        let pr = recover("} flow F() { }");
11780        // Recovery must keep walking past stray `}`.
11781        let names: Vec<&str> = pr
11782            .program
11783            .declarations
11784            .iter()
11785            .filter_map(|d| match d {
11786                Declaration::Flow(f) => Some(f.name.as_str()),
11787                _ => None,
11788            })
11789            .collect();
11790        assert!(names.contains(&"F"), "F not recovered: {names:?}");
11791    }
11792
11793    #[test]
11794    fn error_at_eof_does_not_loop() {
11795        // Truncated declaration. Must terminate; finite errors.
11796        let pr = recover("flow F() { ");
11797        // Either errored or somehow accepted — but must terminate.
11798        let _ = pr.errors.len();
11799    }
11800
11801    #[test]
11802    fn nested_braces_inside_error_still_balance() {
11803        // Walker must respect brace depth so a `}` inside a malformed
11804        // block does not prematurely sync.
11805        let src = "flow F() { not_a_step { inner } } flow G() { }";
11806        let pr = recover(src);
11807        let names: Vec<&str> = pr
11808            .program
11809            .declarations
11810            .iter()
11811            .filter_map(|d| match d {
11812                Declaration::Flow(f) => Some(f.name.as_str()),
11813                _ => None,
11814            })
11815            .collect();
11816        assert!(names.contains(&"G"), "G not recovered: {names:?}");
11817    }
11818
11819    // ── robustness_fuzz ──────────────────────────────────────────
11820    //
11821    // Deterministic-seeded mutator (xorshift). 100 buckets ×
11822    // 10 mutations = 1000 iterations, byte-bounded so fuzz time
11823    // stays under 1 s on a release build. Recovery must NEVER crash;
11824    // lexer-level errors are out of scope (lexer recovery is its own
11825    // sub-fase). 28.b mirrors this with the same structure.
11826
11827    #[derive(Clone, Copy)]
11828    struct Xorshift(u64);
11829    impl Xorshift {
11830        fn next(&mut self) -> u64 {
11831            let mut x = self.0;
11832            x ^= x << 13;
11833            x ^= x >> 7;
11834            x ^= x << 17;
11835            self.0 = x;
11836            x
11837        }
11838        fn pick<T: Copy>(&mut self, slice: &[T]) -> T {
11839            slice[(self.next() as usize) % slice.len()]
11840        }
11841    }
11842
11843    fn mutate(src: &str, rng: &mut Xorshift) -> String {
11844        let mut bytes: Vec<u8> = src.bytes().collect();
11845        if bytes.is_empty() {
11846            return src.to_string();
11847        }
11848        let op = rng.next() % 4;
11849        let pos = (rng.next() as usize) % bytes.len();
11850        // Stick to ASCII-safe printable bytes to keep input lex-able
11851        // most of the time. AxonLexerError is still possible and is
11852        // tolerated by the recovery contract.
11853        let safe: &[u8] = b"abcdefghijklmnopqrstuvwxyz {}();:,_0123456789";
11854        match op {
11855            0 => {
11856                bytes.remove(pos);
11857            }
11858            1 => {
11859                let b = rng.pick(safe);
11860                bytes.insert(pos, b);
11861            }
11862            2 if pos + 1 < bytes.len() => {
11863                bytes.swap(pos, pos + 1);
11864            }
11865            _ => {
11866                let b = rng.pick(safe);
11867                bytes[pos] = b;
11868            }
11869        }
11870        // Lossy decode: mutator may have produced invalid UTF-8;
11871        // strip non-ASCII before handing to the lexer.
11872        bytes.retain(|b| b.is_ascii());
11873        String::from_utf8_lossy(&bytes).into_owned()
11874    }
11875
11876    #[test]
11877    fn fuzz_recovery_never_crashes() {
11878        let seed_bases = [
11879            "flow F() { }",
11880            "intent I { }",
11881            "persona P { name: \"P\" role: \"R\" }",
11882            "intent J { ask: \"a\" }",
11883            "type T = String",
11884        ];
11885        // 100 buckets × 10 mutations = 1000 iterations, deterministic.
11886        for (bucket, base) in (0..100u64).zip(seed_bases.iter().cycle()) {
11887            let mut rng = Xorshift(0x1234_5678_9abc_def0_u64.wrapping_add(bucket));
11888            let mut current = (*base).to_string();
11889            for _ in 0..10 {
11890                current = mutate(&current, &mut rng);
11891                // Lexer may reject; that's outside parser-recovery
11892                // scope (28.b/c). Skip those iterations.
11893                let toks = match Lexer::new(&current, "<fuzz>").tokenize() {
11894                    Ok(t) => t,
11895                    Err(_) => continue,
11896                };
11897                // Recovery must not panic on any well-lexed input.
11898                let _pr = Parser::new(toks).parse_with_recovery();
11899            }
11900        }
11901    }
11902
11903    // ── integration_with_v1_19_4_colon_diagnostic ────────────────
11904
11905    #[test]
11906    fn missing_colon_hint_preserved_under_recovery() {
11907        // The Rust frontend's strict `parse()` carries the same
11908        // colon diagnostic shape as the Python side. Recovery mode
11909        // must not erase it.
11910        let src = "flow F() { run R { input { user_message: \"hi\" } } }";
11911        let pr = recover(src);
11912        // Either the parser accepts this (some shape may be valid)
11913        // or it errors — but if it errors, the message must surface
11914        // the diagnostic content.
11915        if !pr.errors.is_empty() {
11916            let any_msg = pr.errors.iter().any(|e| !e.message.is_empty());
11917            assert!(any_msg);
11918        }
11919    }
11920
11921    // ── recovery preserves declaration ordering ──────────────────
11922
11923    #[test]
11924    fn recovered_declarations_appear_in_source_order() {
11925        let src = "flow A() { } garbage flow B() { } garbage flow C() { }";
11926        let pr = recover(src);
11927        let names: Vec<&str> = pr
11928            .program
11929            .declarations
11930            .iter()
11931            .filter_map(|d| match d {
11932                Declaration::Flow(f) => Some(f.name.as_str()),
11933                _ => None,
11934            })
11935            .collect();
11936        assert_eq!(names, vec!["A", "B", "C"]);
11937    }
11938}
11939
11940// ── §Fase 28.d — Source-context diagnostic block test pack ───────────────────
11941//
11942// Mirror of `tests/test_fase28_source_context.py` (Python side, 28.d).
11943// The render output must be byte-identical to the Python `SourceSnippet.render`
11944// on the same input — D7 ratified (cross-stack drift gate). Golden strings
11945// in `golden_*` tests are duplicated verbatim in the Python pack; edits
11946// here MUST be mirrored on the Python side and vice versa.
11947#[cfg(test)]
11948mod fase28_source_context_tests {
11949    use super::*;
11950    use crate::lexer::Lexer;
11951
11952    fn snippet(source: &str, line: u32, column: u32, filename: &str) -> String {
11953        SourceSnippet::new(
11954            source.to_string(),
11955            line,
11956            column,
11957            filename.to_string(),
11958        )
11959        .render()
11960    }
11961
11962    // ── Pure rendering ──────────────────────────────────────────
11963
11964    #[test]
11965    fn rustc_style_block_for_middle_line() {
11966        let src = "line one\nline two\nline three\nline four\nline five";
11967        let out = snippet(src, 3, 6, "x.axon");
11968        assert!(out.contains("--> x.axon:3:6"));
11969        assert!(out.contains("1 | line one"));
11970        assert!(out.contains("2 | line two"));
11971        assert!(out.contains("3 | line three"));
11972        assert!(out.contains("4 | line four"));
11973        assert!(out.contains("5 | line five"));
11974        // Caret col 6 → 5-space pad. Empty gutter is 1 space (gutter=1).
11975        assert!(out.contains("\n  |      ^"), "out:\n{out}");
11976    }
11977
11978    #[test]
11979    fn caret_column_one_renders_correctly() {
11980        let out = snippet("abc\n", 1, 1, "<source>");
11981        assert!(out.contains("\n  | ^"));
11982    }
11983
11984    #[test]
11985    fn first_line_clamps_context_before_to_zero() {
11986        let src = "first\nsecond\nthird\nfourth\nfifth";
11987        let out = snippet(src, 1, 1, "<source>");
11988        assert!(out.contains("1 | first"));
11989        assert!(out.contains("2 | second"));
11990        assert!(out.contains("3 | third"));
11991        assert!(!out.contains("4 | fourth"));
11992    }
11993
11994    #[test]
11995    fn last_line_clamps_context_after_to_eof() {
11996        let src = "first\nsecond\nthird\nfourth\nfifth";
11997        let out = snippet(src, 5, 2, "<source>");
11998        assert!(out.contains("5 | fifth"));
11999        assert!(out.contains("3 | third"));
12000        assert!(out.contains("4 | fourth"));
12001        assert!(!out.contains("2 | second"));
12002    }
12003
12004    #[test]
12005    fn gutter_width_grows_with_line_count() {
12006        let src: String = (1..=12).map(|i| format!("line{i}")).collect::<Vec<_>>().join("\n");
12007        let out = snippet(&src, 12, 1, "<source>");
12008        assert!(out.contains("12 | line12"));
12009        assert!(out.contains("10 | line10"));
12010    }
12011
12012    // ── Edge cases ──────────────────────────────────────────────
12013
12014    #[test]
12015    fn empty_source_returns_empty() {
12016        assert_eq!(snippet("", 1, 1, "<source>"), "");
12017    }
12018
12019    #[test]
12020    fn zero_line_returns_empty() {
12021        assert_eq!(snippet("hi", 0, 1, "<source>"), "");
12022    }
12023
12024    #[test]
12025    fn out_of_range_line_returns_empty() {
12026        assert_eq!(snippet("hi", 99, 1, "<source>"), "");
12027    }
12028
12029    #[test]
12030    fn caret_clamps_past_eol() {
12031        let out = snippet("hello", 1, 50, "<source>");
12032        assert!(out.contains("\n  |      ^"), "out:\n{out}");
12033    }
12034
12035    #[test]
12036    fn unicode_codepoint_count_for_caret_clamp() {
12037        // "héllo" = 5 codepoints; column past EOL clamps to 6.
12038        let out = snippet("héllo", 1, 99, "<source>");
12039        assert!(out.contains("\n  |      ^"), "out:\n{out}");
12040    }
12041
12042    #[test]
12043    fn trailing_newline_does_not_create_phantom_last_line() {
12044        let out = snippet("first\nsecond\n", 2, 1, "<source>");
12045        assert!(!out.contains("3 |"));
12046        assert!(out.contains("2 | second"));
12047    }
12048
12049    // ── Parser attach plumbing ──────────────────────────────────
12050
12051    fn lex(src: &str) -> Vec<Token> {
12052        Lexer::new(src, "<test>").tokenize().expect("lex")
12053    }
12054
12055    #[test]
12056    fn strict_parse_attaches_snippet_when_source_given() {
12057        let src = "garbage_token\nflow F() { }";
12058        let err = Parser::new(lex(src))
12059            .with_source(src, "x.axon")
12060            .parse()
12061            .expect_err("must error");
12062        assert!(err.source_snippet.is_some());
12063        let display = format!("{err}");
12064        assert!(display.contains("--> x.axon:"), "display: {display}");
12065    }
12066
12067    #[test]
12068    fn strict_parse_no_snippet_when_no_source() {
12069        let src = "garbage_token";
12070        let err = Parser::new(lex(src)).parse().expect_err("must error");
12071        assert!(err.source_snippet.is_none());
12072        let display = format!("{err}");
12073        assert!(!display.contains("\n  -->"));
12074    }
12075
12076    #[test]
12077    fn every_recovered_error_has_snippet() {
12078        let src = "garbage1\nflow F() { }\ngarbage2\nflow G() { }";
12079        let result = Parser::new(lex(src))
12080            .with_source(src, "multi.axon")
12081            .parse_with_recovery();
12082        assert!(!result.errors.is_empty());
12083        for err in &result.errors {
12084            assert!(err.source_snippet.is_some());
12085            let display = format!("{err}");
12086            assert!(
12087                display.contains("--> multi.axon:"),
12088                "display: {display}"
12089            );
12090        }
12091    }
12092
12093    #[test]
12094    fn recovery_no_snippet_when_no_source() {
12095        let src = "garbage1 garbage2";
12096        let result = Parser::new(lex(src)).parse_with_recovery();
12097        for err in &result.errors {
12098            assert!(err.source_snippet.is_none());
12099        }
12100    }
12101
12102    #[test]
12103    fn snippet_points_at_correct_line_for_each_error() {
12104        let src = "garbage_a\nflow F() { }\ngarbage_b\nflow G() { }";
12105        let result = Parser::new(lex(src))
12106            .with_source(src, "x")
12107            .parse_with_recovery();
12108        for err in &result.errors {
12109            let sn = err.source_snippet.as_ref().expect("snippet");
12110            assert_eq!(sn.line, err.line);
12111        }
12112    }
12113
12114    // ── Backwards-compat ────────────────────────────────────────
12115
12116    #[test]
12117    fn legacy_constructor_still_works() {
12118        let src = "flow F() { }";
12119        let prog = Parser::new(lex(src)).parse().expect("clean");
12120        assert_eq!(prog.declarations.len(), 1);
12121    }
12122
12123    #[test]
12124    fn attach_source_idempotent() {
12125        let err = ParseError {
12126            message: "bad".to_string(),
12127            line: 2,
12128            column: 3,
12129            ..Default::default()
12130        };
12131        let err2 = err.clone().attach_source("a\nb\nc\n", "f.axon");
12132        let first = format!("{err2}");
12133        let err3 = err.attach_source("a\nb\nc\n", "f.axon");
12134        let second = format!("{err3}");
12135        assert_eq!(first, second);
12136    }
12137
12138    #[test]
12139    fn attach_source_noop_when_line_zero() {
12140        let err = ParseError {
12141            message: "bad".to_string(),
12142            line: 0,
12143            column: 0,
12144            ..Default::default()
12145        };
12146        let err = err.attach_source("a\nb\nc\n", "f.axon");
12147        assert!(err.source_snippet.is_none());
12148    }
12149
12150    // ── Cross-stack golden parity ───────────────────────────────
12151    // These golden strings are duplicated verbatim in the Python
12152    // test pack at `tests/test_fase28_source_context.py::TestRustParityShape`.
12153    // Edits here MUST be mirrored in the Python pack — D7.
12154
12155    #[test]
12156    fn golden_simple_three_line_block() {
12157        let src = "alpha\nbeta\ngamma";
12158        let out = snippet(src, 2, 3, "g.axon");
12159        // Note: gutter=1, so empty_gutter=" " (one space). The
12160        // " --> ..." line therefore starts with two spaces ("<empty>"
12161        // + literal " --> ...").
12162        let expected = concat!(
12163            "  --> g.axon:2:3\n",
12164            "  |\n",
12165            "1 | alpha\n",
12166            "2 | beta\n",
12167            "  |   ^\n",
12168            "3 | gamma",
12169        );
12170        assert_eq!(out, expected);
12171    }
12172
12173    #[test]
12174    fn golden_first_line_caret() {
12175        let src = "abc\ndef\n";
12176        let out = snippet(src, 1, 1, "x");
12177        let expected = concat!(
12178            "  --> x:1:1\n",
12179            "  |\n",
12180            "1 | abc\n",
12181            "  | ^\n",
12182            "2 | def",
12183        );
12184        assert_eq!(out, expected);
12185    }
12186
12187    #[test]
12188    fn golden_two_digit_gutter() {
12189        let src: String = (1..=11)
12190            .map(|i| format!("L{i}"))
12191            .collect::<Vec<_>>()
12192            .join("\n");
12193        let out = snippet(&src, 10, 2, "big");
12194        let expected = concat!(
12195            "   --> big:10:2\n",
12196            "   |\n",
12197            " 8 | L8\n",
12198            " 9 | L9\n",
12199            "10 | L10\n",
12200            "   |  ^\n",
12201            "11 | L11",
12202        );
12203        assert_eq!(out, expected);
12204    }
12205}
12206
12207// ── §Fase 28.e — Parser integration tests for smart-suggest ──────────────────
12208//
12209// Mirror of `tests/test_fase28_smart_suggest.py::TestParserIntegration`.
12210// Verifies that the parser actually wires `suggest_for` into the
12211// unknown-keyword diagnostic at both error sites — top-level and
12212// flow-body.
12213#[cfg(test)]
12214mod fase28_smart_suggest_parser_tests {
12215    use super::*;
12216    use crate::lexer::Lexer;
12217
12218    fn lex(src: &str) -> Vec<Token> {
12219        Lexer::new(src, "<test>").tokenize().expect("lex")
12220    }
12221
12222    #[test]
12223    fn top_level_typo_suggests_flow() {
12224        let src = "flwo F() { }";
12225        let err = Parser::new(lex(src)).parse().expect_err("must error");
12226        assert!(
12227            err.message.contains("Did you mean `flow`?"),
12228            "msg: {}",
12229            err.message
12230        );
12231    }
12232
12233    #[test]
12234    fn top_level_unknown_far_no_suggestion() {
12235        let src = "qwerty F() { }";
12236        let err = Parser::new(lex(src)).parse().expect_err("must error");
12237        assert!(
12238            !err.message.contains("Did you mean"),
12239            "msg: {}",
12240            err.message
12241        );
12242    }
12243
12244    #[test]
12245    fn flow_body_typo_suggests_step() {
12246        let src = "flow F() { stepp S {} }";
12247        let err = Parser::new(lex(src)).parse().expect_err("must error");
12248        assert!(
12249            err.message.contains("Did you mean `step`"),
12250            "msg: {}",
12251            err.message
12252        );
12253    }
12254
12255    #[test]
12256    fn flow_body_typo_suggests_reason() {
12257        let src = "flow F() { reasn R {} }";
12258        let err = Parser::new(lex(src)).parse().expect_err("must error");
12259        assert!(
12260            err.message.contains("Did you mean `reason`?"),
12261            "msg: {}",
12262            err.message
12263        );
12264    }
12265
12266    #[test]
12267    fn recovery_mode_carries_hint() {
12268        let src = "flwo F() { }";
12269        let result = Parser::new(lex(src)).parse_with_recovery();
12270        assert!(
12271            result
12272                .errors
12273                .iter()
12274                .any(|e| e.message.contains("Did you mean `flow`?")),
12275            "errors: {:?}",
12276            result.errors
12277        );
12278    }
12279}
12280
12281// ── §Fase 35.m — mutate / purge where-clause capture ────────────────
12282
12283#[cfg(test)]
12284mod fase35m_mutate_purge_where_tests {
12285    use super::*;
12286
12287    fn parse(src: &str) -> Program {
12288        let tokens = crate::lexer::Lexer::new(src, "<test>")
12289            .tokenize()
12290            .expect("lex");
12291        Parser::new(tokens).parse().expect("parse")
12292    }
12293
12294    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
12295        for d in &prog.declarations {
12296            if let Declaration::Flow(f) = d {
12297                if f.name == flow {
12298                    return f.body.first().expect("flow has at least one step");
12299                }
12300            }
12301        }
12302        panic!("flow `{flow}` not found");
12303    }
12304
12305    #[test]
12306    fn mutate_captures_its_where_clause() {
12307        // Pre-35.m the `{ where: }` block was skipped — every mutate
12308        // ran whole-store. It must now reach `where_expr`.
12309        let prog =
12310            parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
12311        match first_step(&prog, "F") {
12312            FlowStep::Mutate(m) => {
12313                assert_eq!(m.store_name, "accounts");
12314                assert_eq!(m.where_expr, "id = 1");
12315            }
12316            other => panic!("expected Mutate, got {other:?}"),
12317        }
12318    }
12319
12320    #[test]
12321    fn purge_captures_its_where_clause() {
12322        let prog =
12323            parse("flow F() -> Unit { purge logs { where: \"ts < 100\" } }");
12324        match first_step(&prog, "F") {
12325            FlowStep::Purge(p) => {
12326                assert_eq!(p.store_name, "logs");
12327                assert_eq!(p.where_expr, "ts < 100");
12328            }
12329            other => panic!("expected Purge, got {other:?}"),
12330        }
12331    }
12332
12333    #[test]
12334    fn mutate_without_a_where_block_is_a_whole_store_op() {
12335        // No `{ where: }` → an empty filter → the runtime renders
12336        // `WHERE TRUE` (every row). A valid, intentional op.
12337        let prog = parse("flow F() -> Unit { mutate accounts }");
12338        match first_step(&prog, "F") {
12339            FlowStep::Mutate(m) => {
12340                assert_eq!(m.store_name, "accounts");
12341                assert_eq!(m.where_expr, "");
12342            }
12343            other => panic!("expected Mutate, got {other:?}"),
12344        }
12345    }
12346}
12347
12348// ── §Fase 35.o — persist field-block capture ────────────────────────
12349
12350#[cfg(test)]
12351mod fase35o_persist_fields_tests {
12352    use super::*;
12353
12354    fn parse(src: &str) -> Program {
12355        let tokens = crate::lexer::Lexer::new(src, "<test>")
12356            .tokenize()
12357            .expect("lex");
12358        Parser::new(tokens).parse().expect("parse")
12359    }
12360
12361    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
12362        for d in &prog.declarations {
12363            if let Declaration::Flow(f) = d {
12364                if f.name == flow {
12365                    return f.body.first().expect("flow has at least one step");
12366                }
12367            }
12368        }
12369        panic!("flow `{flow}` not found");
12370    }
12371
12372    #[test]
12373    fn persist_captures_its_field_block() {
12374        // Pre-35.o the `{ col: value }` block was skipped — every
12375        // persist wrote the whole binding context. It must now reach
12376        // `fields`, in source order, with value expressions raw.
12377        let prog = parse(
12378            "flow F() -> Unit { persist into chat_history { \
12379             session_id: \"${session_id}\" sender: \"user\" \
12380             content: \"${message}\" } }",
12381        );
12382        match first_step(&prog, "F") {
12383            FlowStep::Persist(p) => {
12384                assert_eq!(p.store_name, "chat_history");
12385                assert_eq!(
12386                    p.fields,
12387                    vec![
12388                        ("session_id".to_string(), "${session_id}".to_string()),
12389                        ("sender".to_string(), "user".to_string()),
12390                        ("content".to_string(), "${message}".to_string()),
12391                    ]
12392                );
12393            }
12394            other => panic!("expected Persist, got {other:?}"),
12395        }
12396    }
12397
12398    #[test]
12399    fn persist_without_a_block_keeps_the_user_bindings_fallback() {
12400        // No `{ }` → empty `fields` → the runtime falls back to the
12401        // v1.30.0 user-bindings row. Backward-compatible.
12402        let prog = parse("flow F() -> Unit { persist events }");
12403        match first_step(&prog, "F") {
12404            FlowStep::Persist(p) => {
12405                assert_eq!(p.store_name, "events");
12406                assert!(p.fields.is_empty());
12407            }
12408            other => panic!("expected Persist, got {other:?}"),
12409        }
12410    }
12411
12412    #[test]
12413    fn persist_accepts_the_optional_into_connector() {
12414        // `persist into X` and `persist X` resolve to the SAME store
12415        // name — pre-35.o `into` was captured AS the store name.
12416        let with =
12417            parse("flow F() -> Unit { persist into accounts { id: \"1\" } }");
12418        let without =
12419            parse("flow F() -> Unit { persist accounts { id: \"1\" } }");
12420        for prog in [&with, &without] {
12421            match first_step(prog, "F") {
12422                FlowStep::Persist(p) => assert_eq!(p.store_name, "accounts"),
12423                other => panic!("expected Persist, got {other:?}"),
12424            }
12425        }
12426    }
12427
12428    #[test]
12429    fn persist_into_without_a_block_resolves_the_store_name() {
12430        // `persist into events` — the `into` connector is skipped, the
12431        // store name is `events` (not `into`). Lateral bug closed.
12432        let prog = parse("flow F() -> Unit { persist into events }");
12433        match first_step(&prog, "F") {
12434            FlowStep::Persist(p) => {
12435                assert_eq!(p.store_name, "events");
12436                assert!(p.fields.is_empty());
12437            }
12438            other => panic!("expected Persist, got {other:?}"),
12439        }
12440    }
12441
12442    #[test]
12443    fn persist_fields_lower_into_the_ir() {
12444        // The IR generator must carry `fields` onto `IRPersistStep`
12445        // so the runtime reads exactly the declared columns.
12446        let prog = parse(
12447            "flow F() -> Unit { persist into chat { content: \"${msg}\" } }",
12448        );
12449        let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
12450        let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
12451        match flow.steps.first().expect("one step") {
12452            crate::ir_nodes::IRFlowNode::Persist(p) => {
12453                assert_eq!(p.store_name, "chat");
12454                assert_eq!(
12455                    p.fields,
12456                    vec![("content".to_string(), "${msg}".to_string())]
12457                );
12458            }
12459            other => panic!("expected IRFlowNode::Persist, got {other:?}"),
12460        }
12461    }
12462}
12463
12464// ── §Fase 35.p — mutate SET-field-block capture ─────────────────────
12465
12466#[cfg(test)]
12467mod fase35p_mutate_fields_tests {
12468    use super::*;
12469
12470    fn parse(src: &str) -> Program {
12471        let tokens = crate::lexer::Lexer::new(src, "<test>")
12472            .tokenize()
12473            .expect("lex");
12474        Parser::new(tokens).parse().expect("parse")
12475    }
12476
12477    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
12478        for d in &prog.declarations {
12479            if let Declaration::Flow(f) = d {
12480                if f.name == flow {
12481                    return f.body.first().expect("flow has at least one step");
12482                }
12483            }
12484        }
12485        panic!("flow `{flow}` not found");
12486    }
12487
12488    #[test]
12489    fn mutate_captures_its_set_field_block() {
12490        // Pre-35.p every key but `where:` was skipped — the runtime
12491        // SET every flow binding. The SET columns must now reach
12492        // `fields`, in source order, with `where:` still captured.
12493        let prog = parse(
12494            "flow F() -> Unit { mutate accounts { where: \"id = ${id}\" \
12495             balance: \"${new_balance}\" status: \"active\" } }",
12496        );
12497        match first_step(&prog, "F") {
12498            FlowStep::Mutate(m) => {
12499                assert_eq!(m.store_name, "accounts");
12500                assert_eq!(m.where_expr, "id = ${id}");
12501                assert_eq!(
12502                    m.fields,
12503                    vec![
12504                        ("balance".to_string(), "${new_balance}".to_string()),
12505                        ("status".to_string(), "active".to_string()),
12506                    ]
12507                );
12508            }
12509            other => panic!("expected Mutate, got {other:?}"),
12510        }
12511    }
12512
12513    #[test]
12514    fn mutate_where_only_block_has_no_set_fields() {
12515        // A `{ where: }`-only block → empty `fields` → the runtime
12516        // falls back to the v1.31.0 user-bindings SET.
12517        let prog =
12518            parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
12519        match first_step(&prog, "F") {
12520            FlowStep::Mutate(m) => {
12521                assert_eq!(m.where_expr, "id = 1");
12522                assert!(m.fields.is_empty());
12523            }
12524            other => panic!("expected Mutate, got {other:?}"),
12525        }
12526    }
12527
12528    #[test]
12529    fn mutate_with_no_block_is_a_whole_store_op() {
12530        // No block at all → empty where + empty fields (a whole-store
12531        // UPDATE from user bindings) — unchanged from 35.m.
12532        let prog = parse("flow F() -> Unit { mutate accounts }");
12533        match first_step(&prog, "F") {
12534            FlowStep::Mutate(m) => {
12535                assert_eq!(m.store_name, "accounts");
12536                assert_eq!(m.where_expr, "");
12537                assert!(m.fields.is_empty());
12538            }
12539            other => panic!("expected Mutate, got {other:?}"),
12540        }
12541    }
12542
12543    #[test]
12544    fn mutate_fields_lower_into_the_ir() {
12545        let prog = parse(
12546            "flow F() -> Unit { mutate t { where: \"id = 1\" v: \"${x}\" } }",
12547        );
12548        let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
12549        let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
12550        match flow.steps.first().expect("one step") {
12551            crate::ir_nodes::IRFlowNode::Mutate(m) => {
12552                assert_eq!(m.where_expr, "id = 1");
12553                assert_eq!(
12554                    m.fields,
12555                    vec![("v".to_string(), "${x}".to_string())]
12556                );
12557            }
12558            other => panic!("expected IRFlowNode::Mutate, got {other:?}"),
12559        }
12560    }
12561}
12562