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/// Bemarking AI'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 14.a — leading trivia parallel array, indexed by the
1441    /// effective-token position. `leading_trivia[i]` is the comment
1442    /// trivia that appeared between the previous effective token (or
1443    /// file start) and `tokens[i]`.
1444    leading_trivia: Vec<Vec<Trivia>>,
1445    /// Fase 14.a — trailing trivia parallel array. `trailing_trivia[i]`
1446    /// is the comment trivia on the same line as `tokens[i]`, before
1447    /// the next effective token. Populated by the constructor.
1448    trailing_trivia: Vec<Vec<Trivia>>,
1449    /// Fase 17.a — side-channel for tagging let value_kind. Set by
1450    /// `parse_let_atom` / `parse_let_value_expr` as they descend; read
1451    /// at the end of `parse_let` and stored on the LetStatement.
1452    last_let_value_kind: String,
1453    /// Fase 19.e — loop nesting depth for break/continue scope check.
1454    /// Incremented at the start of `parse_for_in`, decremented after.
1455    /// `parse_break`/`parse_continue` raise ParseError when this is
1456    /// zero (the keyword has no meaning outside a loop body).
1457    loop_depth: u32,
1458    /// §Fase 28.d — Optional source text + filename for the rustc-
1459    /// style source-context block on `ParseError`. Set via the
1460    /// fluent `Parser::with_source` builder; default `None` keeps
1461    /// existing callers (`Parser::new(tokens).parse()`) emitting
1462    /// the legacy single-line shape.
1463    source: Option<String>,
1464    filename: String,
1465}
1466
1467impl Parser {
1468    pub fn new(raw_tokens: Vec<Token>) -> Self {
1469        // ── Fase 14.a — split the raw token stream into:
1470        //   - effective tokens the grammar consumes (cursor advances
1471        //     over these as before),
1472        //   - parallel `leading_trivia` / `trailing_trivia` arrays
1473        //     indexed by effective-token position.
1474        // Comments on a fresh line attach as leading trivia of the
1475        // next effective token; comments on the same line as an
1476        // effective token attach as trailing trivia of that token.
1477        // Roslyn/Swift convention.
1478        let mut effective: Vec<Token> = Vec::with_capacity(raw_tokens.len());
1479        let mut leading: Vec<Vec<Trivia>> = Vec::with_capacity(raw_tokens.len());
1480        let mut trailing: Vec<Vec<Trivia>> = Vec::with_capacity(raw_tokens.len());
1481
1482        let mut pending_leading: Vec<Trivia> = Vec::new();
1483        let mut last_effective_line: i64 = -1;
1484        for tok in raw_tokens {
1485            if is_comment_token(&tok.ttype) {
1486                let kind = token_to_trivia_kind(&tok.ttype)
1487                    .expect("comment token must map to a trivia kind");
1488                let triv = Trivia {
1489                    kind,
1490                    text: tok.value,
1491                    line: tok.line,
1492                    column: tok.column,
1493                };
1494                if !effective.is_empty() && (tok.line as i64) == last_effective_line {
1495                    trailing.last_mut().unwrap().push(triv);
1496                } else {
1497                    pending_leading.push(triv);
1498                }
1499            } else {
1500                last_effective_line = tok.line as i64;
1501                effective.push(tok);
1502                leading.push(std::mem::take(&mut pending_leading));
1503                trailing.push(Vec::new());
1504            }
1505        }
1506
1507        Parser {
1508            tokens: effective,
1509            pos: 0,
1510            leading_trivia: leading,
1511            trailing_trivia: trailing,
1512            last_let_value_kind: "literal".to_string(),
1513            loop_depth: 0,
1514            source: None,
1515            filename: "<source>".to_string(),
1516        }
1517    }
1518
1519    /// §Fase 28.d — Fluent attach of source text + filename for
1520    /// rustc-style source-context blocks on emitted `ParseError`s.
1521    /// Returns `self` so it chains with `.parse_with_recovery()`:
1522    ///
1523    /// ```ignore
1524    /// let result = Parser::new(tokens)
1525    ///     .with_source(src, "foo.axon")
1526    ///     .parse_with_recovery();
1527    /// ```
1528    ///
1529    /// No-op of any other behaviour — pure metadata attach.
1530    #[must_use]
1531    pub fn with_source(mut self, source: &str, filename: &str) -> Self {
1532        self.source = Some(source.to_string());
1533        self.filename = filename.to_string();
1534        self
1535    }
1536
1537    // ── public API ───────────────────────────────────────────────
1538
1539    pub fn parse(&mut self) -> Result<Program, ParseError> {
1540        let mut program = Program {
1541            declarations: Vec::new(),
1542            declaration_trivia: Vec::new(),
1543            loc: Loc { line: 1, column: 1 },
1544        };
1545        while !self.check(TokenType::Eof) {
1546            // Capture trivia around the declaration. `start_pos` is
1547            // the effective-token position of the declaration's first
1548            // token; that position carries the leading trivia. After
1549            // parsing, `pos - 1` is the last token consumed; that
1550            // position carries the trailing trivia.
1551            let start_pos = self.pos;
1552            let mut decl = match self.parse_declaration() {
1553                Ok(d) => d,
1554                Err(e) => return Err(self.attach_source_to_error(e)),
1555            };
1556            let end_pos = self.pos.saturating_sub(1);
1557            let leading = self
1558                .leading_trivia
1559                .get(start_pos)
1560                .cloned()
1561                .unwrap_or_default();
1562            let trailing = self
1563                .trailing_trivia
1564                .get(end_pos)
1565                .cloned()
1566                .unwrap_or_default();
1567            // Fase 14.b — also copy trivia into the per-struct fields on
1568            // the declaration so consumers can read `flow.leading_trivia`
1569            // directly without going through `program.declaration_trivia[i]`.
1570            // The side-channel is preserved for backward compat with
1571            // 14.a callers and as a flat enumeration source.
1572            attach_trivia_to_decl(&mut decl, leading.clone(), trailing.clone());
1573            program.declarations.push(decl);
1574            program
1575                .declaration_trivia
1576                .push(DeclarationTrivia { leading, trailing });
1577        }
1578        // §Fase 80.g — expand `voice` declarations FIRST (they may emit
1579        // `from Preset@vN` upstream legs), then §80.f preset references,
1580        // BEFORE type-check — so the §80.c laws and the IR see the expanded
1581        // program (and `axon desugar` prints exactly this lowering).
1582        // Unknown presets stay unexpanded — the checker reports them with
1583        // the catalog list (accumulating diagnostics beat a parse abort).
1584        crate::voice_desugar::expand(&mut program);
1585        crate::upstream_presets::expand(&mut program);
1586        Ok(program)
1587    }
1588
1589    // ── §Fase 28.c — recovery-mode parse ─────────────────────────
1590    //
1591    // Mirror of Python's `Parser.parse_with_recovery` from
1592    // `axon/compiler/parser.py`. Wraps `parse_declaration` in a
1593    // try/recover loop: on any `ParseError` the error is appended to
1594    // the list and the cursor advances to the next sync point, then
1595    // parsing resumes. The two stacks must produce structurally
1596    // identical error lists on the same input — that is the cross-
1597    // stack drift gate (D7). See the test module
1598    // `tests::fase28_recovery_tests` and Python-side
1599    // `tests/test_fase28_parser_recovery.py`.
1600
1601    /// Recovery-mode parse. Collects every parse error in source
1602    /// order; the existing `parse()` API remains fail-fast (D9).
1603    ///
1604    /// # Recovery contract (D2)
1605    ///
1606    /// On `ParseError`:
1607    ///   1. Push the error onto `errors`.
1608    ///   2. If the cursor is already on a top-level declaration
1609    ///      keyword (and brace-depth ≤ 0), do not consume — the
1610    ///      caller should retry the declaration parse from here.
1611    ///      Otherwise advance one token to make progress, then
1612    ///      walk to the next sync point.
1613    ///   3. Resume the outer loop.
1614    ///
1615    /// Sync points: top-level declaration keyword at brace-depth ≤ 0,
1616    /// or EOF. Negative depths are treated identically to ≤ 0 — the
1617    /// walker keeps walking through over-balanced `}` rather than
1618    /// pretending a closing brace is itself a sync point (which would
1619    /// emit a ghost "Unexpected token at top level" error in the
1620    /// outer loop).
1621    pub fn parse_with_recovery(&mut self) -> ParseResult {
1622        let mut program = Program {
1623            declarations: Vec::new(),
1624            declaration_trivia: Vec::new(),
1625            loc: Loc { line: 1, column: 1 },
1626        };
1627        let mut errors: Vec<ParseError> = Vec::new();
1628
1629        while !self.check(TokenType::Eof) {
1630            let start_pos = self.pos;
1631            match self.parse_declaration() {
1632                Ok(mut decl) => {
1633                    let end_pos = self.pos.saturating_sub(1);
1634                    let leading = self
1635                        .leading_trivia
1636                        .get(start_pos)
1637                        .cloned()
1638                        .unwrap_or_default();
1639                    let trailing = self
1640                        .trailing_trivia
1641                        .get(end_pos)
1642                        .cloned()
1643                        .unwrap_or_default();
1644                    attach_trivia_to_decl(&mut decl, leading.clone(), trailing.clone());
1645                    program.declarations.push(decl);
1646                    program
1647                        .declaration_trivia
1648                        .push(DeclarationTrivia { leading, trailing });
1649                }
1650                Err(err) => {
1651                    // §Fase 28.d — attach source-context block when a
1652                    // source has been provided via `with_source(...)`;
1653                    // otherwise the error keeps its single-line shape.
1654                    errors.push(self.attach_source_to_error(err));
1655                    // Make progress. If parse_declaration returned
1656                    // immediately on the same token (e.g. unknown
1657                    // top-level token), we MUST advance at least one
1658                    // token to avoid an infinite loop.
1659                    if self.pos == start_pos && !self.check(TokenType::Eof) {
1660                        self.advance();
1661                    }
1662                    self.advance_to_sync_point();
1663                }
1664            }
1665        }
1666
1667        ParseResult { program, errors }
1668    }
1669
1670    /// §Fase 28.d — Decorate a `ParseError` with a `SourceSnippet`
1671    /// when the parser has source context attached, otherwise return
1672    /// the error unchanged. Idempotent: if the error already carries
1673    /// a snippet, this overwrites it with the parser's source.
1674    fn attach_source_to_error(&self, err: ParseError) -> ParseError {
1675        match &self.source {
1676            Some(src) if err.line >= 1 => err.attach_source(src, &self.filename),
1677            _ => err,
1678        }
1679    }
1680
1681    /// §Fase 28.c — Walk the cursor forward until the next sync
1682    /// point (top-level declaration keyword at brace-depth ≤ 0) or
1683    /// EOF. Used by `parse_with_recovery` to skip the malformed
1684    /// remainder of a failed declaration.
1685    fn advance_to_sync_point(&mut self) {
1686        let mut depth: i32 = 0;
1687        while !self.check(TokenType::Eof) {
1688            let tt = self.current().ttype.clone();
1689            // Sync at top-level keywords when depth ≤ 0. We do not
1690            // consume the keyword — the outer loop will dispatch on
1691            // it.
1692            if is_top_level_decl_kw_for_recovery(&tt) && depth <= 0 {
1693                return;
1694            }
1695            if matches!(tt, TokenType::LBrace) {
1696                depth += 1;
1697            } else if matches!(tt, TokenType::RBrace) {
1698                depth -= 1;
1699            }
1700            self.advance();
1701        }
1702    }
1703
1704    // ── token helpers ────────────────────────────────────────────
1705
1706    fn current(&self) -> &Token {
1707        if self.pos >= self.tokens.len() {
1708            self.tokens.last().unwrap() // EOF sentinel
1709        } else {
1710            &self.tokens[self.pos]
1711        }
1712    }
1713
1714    fn advance(&mut self) -> &Token {
1715        let idx = self.pos;
1716        if self.pos < self.tokens.len() {
1717            self.pos += 1;
1718        }
1719        &self.tokens[idx]
1720    }
1721
1722    fn check(&self, tt: TokenType) -> bool {
1723        self.current().ttype == tt
1724    }
1725
1726    fn consume(&mut self, expected: TokenType) -> Result<Token, ParseError> {
1727        let tok = self.current().clone();
1728        if tok.ttype != expected {
1729            return Err(ParseError {
1730                message: format!(
1731                    "Expected {:?}, found {:?}('{}')",
1732                    expected, tok.ttype, tok.value
1733                ),
1734                line: tok.line,
1735                column: tok.column,
1736                            ..Default::default()
1737            });
1738        }
1739        self.pos += 1;
1740        Ok(tok)
1741    }
1742
1743    /// §Fase 41.b — build a `ParseError` at the current token's location.
1744    fn error(&self, message: &str) -> ParseError {
1745        let tok = self.current();
1746        ParseError { message: message.to_string(), line: tok.line, column: tok.column, ..Default::default() }
1747    }
1748
1749    /// Consume any identifier or keyword-used-as-value.
1750    fn consume_any_ident_or_kw(&mut self) -> Result<Token, ParseError> {
1751        let tok = self.current().clone();
1752        match tok.ttype {
1753            TokenType::Identifier
1754            | TokenType::Bool
1755            | TokenType::StringLit
1756            | TokenType::Integer
1757            | TokenType::Float => {
1758                self.pos += 1;
1759                Ok(tok)
1760            }
1761            _ => {
1762                // Allow any keyword token whose value is alphabetic
1763                if !tok.value.is_empty()
1764                    && tok.value.chars().all(|c| c.is_alphanumeric() || c == '_')
1765                    && tok.ttype != TokenType::Eof
1766                {
1767                    self.pos += 1;
1768                    Ok(tok)
1769                } else {
1770                    Err(ParseError {
1771                        message: format!(
1772                            "Expected identifier or keyword value, found {:?}('{}')",
1773                            tok.ttype, tok.value
1774                        ),
1775                        line: tok.line,
1776                        column: tok.column,
1777                                            ..Default::default()
1778                    })
1779                }
1780            }
1781        }
1782    }
1783
1784    fn consume_number(&mut self) -> Result<f64, ParseError> {
1785        let tok = self.current().clone();
1786        match tok.ttype {
1787            TokenType::Float | TokenType::Integer => {
1788                self.pos += 1;
1789                tok.value.parse::<f64>().map_err(|_| ParseError {
1790                    message: format!("Invalid number '{}'", tok.value),
1791                    line: tok.line,
1792                    column: tok.column,
1793                                    ..Default::default()
1794                })
1795            }
1796            _ => Err(ParseError {
1797                message: format!("Expected number, found {:?}('{}')", tok.ttype, tok.value),
1798                line: tok.line,
1799                column: tok.column,
1800                            ..Default::default()
1801            }),
1802        }
1803    }
1804
1805    fn parse_bool(&mut self) -> Result<bool, ParseError> {
1806        let tok = self.consume(TokenType::Bool)?;
1807        Ok(tok.value == "true")
1808    }
1809
1810    fn loc_of(&self, tok: &Token) -> Loc {
1811        Loc {
1812            line: tok.line,
1813            column: tok.column,
1814        }
1815    }
1816
1817    fn check_comparison(&self) -> bool {
1818        matches!(
1819            self.current().ttype,
1820            TokenType::Lt
1821                | TokenType::Gt
1822                | TokenType::Lte
1823                | TokenType::Gte
1824                | TokenType::Eq
1825                | TokenType::Neq
1826        )
1827    }
1828
1829    fn check_run_modifier(&self) -> bool {
1830        matches!(
1831            self.current().ttype,
1832            TokenType::As
1833                | TokenType::Within
1834                | TokenType::ConstrainedBy
1835                | TokenType::OnFailure
1836                | TokenType::OutputTo
1837                | TokenType::Effort
1838        )
1839    }
1840
1841    // ── list helpers ─────────────────────────────────────────────
1842
1843    fn parse_string_list(&mut self) -> Result<Vec<String>, ParseError> {
1844        self.consume(TokenType::LBracket)?;
1845        let mut items = Vec::new();
1846        items.push(self.consume(TokenType::StringLit)?.value);
1847        while self.check(TokenType::Comma) {
1848            self.advance();
1849            items.push(self.consume(TokenType::StringLit)?.value);
1850        }
1851        self.consume(TokenType::RBracket)?;
1852        Ok(items)
1853    }
1854
1855    /// §Fase 83.a — a bracketed list of quoted string literals, tolerant of
1856    /// an empty `[]` and a trailing comma before `]` (the `Window.exclude`
1857    /// shape, generalized into a reusable helper). Used for CORS field
1858    /// lists whose values contain characters (`://`, `.`, `-`) that aren't
1859    /// valid bare identifiers — `allow_origins`, `allow_headers`,
1860    /// `expose_headers` — where `parse_string_list`'s "at least one item,
1861    /// no trailing comma" strictness would reject a legitimate empty or
1862    /// comma-terminated declaration.
1863    fn parse_bracketed_strings(&mut self) -> Result<Vec<String>, ParseError> {
1864        self.consume(TokenType::LBracket)?;
1865        let mut items = Vec::new();
1866        if !self.check(TokenType::RBracket) {
1867            items.push(self.consume(TokenType::StringLit)?.value);
1868            while self.check(TokenType::Comma) {
1869                self.advance();
1870                if self.check(TokenType::RBracket) {
1871                    break; // trailing comma
1872                }
1873                items.push(self.consume(TokenType::StringLit)?.value);
1874            }
1875        }
1876        self.consume(TokenType::RBracket)?;
1877        Ok(items)
1878    }
1879
1880    fn parse_identifier_list(&mut self) -> Result<Vec<String>, ParseError> {
1881        let mut names = Vec::new();
1882        names.push(self.consume(TokenType::Identifier)?.value);
1883        while self.check(TokenType::Comma) {
1884            self.advance();
1885            names.push(self.consume(TokenType::Identifier)?.value);
1886        }
1887        Ok(names)
1888    }
1889
1890    fn parse_bracketed_identifiers(&mut self) -> Result<Vec<String>, ParseError> {
1891        self.consume(TokenType::LBracket)?;
1892        let items = self.parse_extended_identifier_list()?;
1893        self.consume(TokenType::RBracket)?;
1894        Ok(items)
1895    }
1896
1897    fn parse_extended_identifier_list(&mut self) -> Result<Vec<String>, ParseError> {
1898        let mut items = Vec::new();
1899        items.push(self.consume_any_ident_or_kw()?.value);
1900        while self.check(TokenType::Comma) {
1901            self.advance();
1902            items.push(self.consume_any_ident_or_kw()?.value);
1903        }
1904        Ok(items)
1905    }
1906
1907    fn parse_dotted_identifier(&mut self) -> Result<String, ParseError> {
1908        let mut parts = vec![self.consume_any_ident_or_kw()?.value];
1909        while self.check(TokenType::Dot) {
1910            self.advance();
1911            parts.push(self.consume_any_ident_or_kw()?.value);
1912        }
1913        Ok(parts.join("."))
1914    }
1915
1916    fn parse_expression_string(&mut self) -> Result<String, ParseError> {
1917        if self.check(TokenType::LBracket) {
1918            let items = self.parse_bracketed_dot_identifiers()?;
1919            return Ok(format!("[{}]", items.join(", ")));
1920        }
1921        self.parse_dotted_identifier()
1922    }
1923
1924    fn parse_bracketed_dot_identifiers(&mut self) -> Result<Vec<String>, ParseError> {
1925        self.consume(TokenType::LBracket)?;
1926        let mut items = vec![self.parse_dotted_identifier()?];
1927        while self.check(TokenType::Comma) {
1928            self.advance();
1929            items.push(self.parse_dotted_identifier()?);
1930        }
1931        self.consume(TokenType::RBracket)?;
1932        Ok(items)
1933    }
1934
1935    fn parse_argument_list(&mut self) -> Result<Vec<String>, ParseError> {
1936        let mut args = Vec::new();
1937        while !self.check(TokenType::RParen) {
1938            let tok = self.current().clone();
1939            match tok.ttype {
1940                TokenType::StringLit | TokenType::Integer | TokenType::Float => {
1941                    self.advance();
1942                    args.push(tok.value);
1943                }
1944                TokenType::Identifier => {
1945                    self.advance();
1946                    let mut val = tok.value;
1947                    if self.check(TokenType::Dot) {
1948                        self.advance();
1949                        val.push('.');
1950                        val.push_str(&self.consume_any_ident_or_kw()?.value);
1951                    }
1952                    args.push(val);
1953                }
1954                _ => {
1955                    self.advance();
1956                    let key = tok.value;
1957                    if self.check(TokenType::Colon) {
1958                        self.advance();
1959                        let v = self.advance().value.clone();
1960                        args.push(format!("{key}:{v}"));
1961                    } else {
1962                        args.push(key);
1963                    }
1964                }
1965            }
1966            if self.check(TokenType::Comma) {
1967                self.advance();
1968            }
1969        }
1970        Ok(args)
1971    }
1972
1973    /// Skip a single value or balanced bracketed/braced block (unknown field).
1974    fn skip_value(&mut self) {
1975        match self.current().ttype {
1976            TokenType::LBracket => {
1977                self.advance();
1978                let mut depth = 1u32;
1979                while depth > 0 && !self.check(TokenType::Eof) {
1980                    if self.check(TokenType::LBracket) {
1981                        depth += 1;
1982                    } else if self.check(TokenType::RBracket) {
1983                        depth -= 1;
1984                    }
1985                    self.advance();
1986                }
1987            }
1988            TokenType::LBrace => {
1989                self.advance();
1990                let mut depth = 1u32;
1991                while depth > 0 && !self.check(TokenType::Eof) {
1992                    if self.check(TokenType::LBrace) {
1993                        depth += 1;
1994                    } else if self.check(TokenType::RBrace) {
1995                        depth -= 1;
1996                    }
1997                    self.advance();
1998                }
1999            }
2000            TokenType::Lt => {
2001                // effect row: <io, network, ...>
2002                self.advance();
2003                let mut depth = 1u32;
2004                while depth > 0 && !self.check(TokenType::Eof) {
2005                    if self.check(TokenType::Lt) {
2006                        depth += 1;
2007                    } else if self.check(TokenType::Gt) {
2008                        depth -= 1;
2009                    }
2010                    self.advance();
2011                }
2012            }
2013            _ => {
2014                self.advance();
2015                while self.check(TokenType::Dot) {
2016                    self.advance();
2017                    self.advance();
2018                }
2019            }
2020        }
2021    }
2022
2023    /// Skip a balanced `{ ... }` block including its braces.
2024    fn skip_braced_block(&mut self) -> Result<(), ParseError> {
2025        self.consume(TokenType::LBrace)?;
2026        let mut depth = 1u32;
2027        while depth > 0 {
2028            if self.check(TokenType::Eof) {
2029                let tok = self.current();
2030                return Err(ParseError {
2031                    message: "Unterminated block — expected '}'".to_string(),
2032                    line: tok.line,
2033                    column: tok.column,
2034                                    ..Default::default()
2035                });
2036            }
2037            if self.check(TokenType::LBrace) {
2038                depth += 1;
2039            } else if self.check(TokenType::RBrace) {
2040                depth -= 1;
2041            }
2042            self.advance();
2043        }
2044        Ok(())
2045    }
2046
2047    fn at_declaration_start(&self) -> bool {
2048        is_declaration_keyword(&self.current().ttype) || self.check(TokenType::Eof)
2049    }
2050
2051    // ── top-level dispatch ───────────────────────────────────────
2052
2053    fn parse_declaration(&mut self) -> Result<Declaration, ParseError> {
2054        let tok = self.current().clone();
2055
2056        // §Fase 114.a — a TOP-LEVEL `budget <Name> { … }`.
2057        //
2058        // `budget` lexes as `TokenType::Budget` (the daemon-field keyword). At top
2059        // level it is only a declaration when a NAME follows — `budget Foo { … }`.
2060        // The lookahead is what keeps the daemon-attached form (`daemon D { budget
2061        // { … } }`, where `{` follows immediately) untouched: there the next token
2062        // is `{`, not an identifier, so this branch does not fire.
2063        if tok.ttype == TokenType::Budget && self.peek_is_identifier() {
2064            return self.parse_top_level_budget().map(Declaration::Budget);
2065        }
2066
2067        match tok.ttype {
2068            TokenType::Import => self.parse_import().map(Declaration::Import),
2069            TokenType::Persona => self.parse_persona().map(Declaration::Persona),
2070            TokenType::Context => self.parse_context().map(Declaration::Context),
2071            TokenType::Anchor => self.parse_anchor().map(Declaration::Anchor),
2072            TokenType::Memory => self.parse_memory().map(Declaration::Memory),
2073            TokenType::Tool => self.parse_tool().map(Declaration::Tool),
2074            TokenType::Type => self.parse_type_def().map(Declaration::Type),
2075            TokenType::Flow => self.parse_flow().map(Declaration::Flow),
2076            TokenType::Intent => self.parse_intent().map(Declaration::Intent),
2077            TokenType::Run => self.parse_run().map(Declaration::Run),
2078            TokenType::Let => self.parse_let().map(Declaration::Let),
2079            TokenType::Know | TokenType::Believe | TokenType::Speculate | TokenType::Doubt => {
2080                self.parse_epistemic_block().map(Declaration::Epistemic)
2081            }
2082            TokenType::Lambda => self.parse_lambda_data().map(Declaration::LambdaData),
2083
2084            // ── Tier 2 declarations (full AST) ──────────────────
2085            TokenType::Agent => self.parse_agent().map(Declaration::Agent),
2086            TokenType::Shield => self.parse_shield().map(Declaration::Shield),
2087            // §Fase 71.a — temporal execution-window guard.
2088            TokenType::Window => self.parse_window().map(Declaration::Window),
2089            TokenType::Pix => self.parse_pix().map(Declaration::Pix),
2090            TokenType::Ledger => self.parse_ledger().map(Declaration::Ledger),
2091            TokenType::Psyche => self.parse_psyche().map(Declaration::Psyche),
2092            TokenType::Corpus => self.parse_corpus().map(Declaration::Corpus),
2093            TokenType::Dataspace => self.parse_dataspace().map(Declaration::Dataspace),
2094            TokenType::Ots => self.parse_ots().map(Declaration::Ots),
2095            TokenType::Mandate => self.parse_mandate().map(Declaration::Mandate),
2096            TokenType::Compute => self.parse_compute().map(Declaration::Compute),
2097            TokenType::Daemon => self.parse_daemon().map(Declaration::Daemon),
2098            TokenType::Extension => self.parse_extension().map(Declaration::Extension),
2099            TokenType::AxonStore => self.parse_axonstore().map(Declaration::AxonStore),
2100            TokenType::AxonEndpoint => self.parse_axonendpoint().map(Declaration::AxonEndpoint),
2101
2102            // ── §λ-L-E Fase 1 — I/O cognitivo ───────────────────
2103            TokenType::Resource => self.parse_resource().map(Declaration::Resource),
2104            TokenType::Fabric => self.parse_fabric().map(Declaration::Fabric),
2105            TokenType::Manifest => self.parse_manifest().map(Declaration::Manifest),
2106            TokenType::Observe => self.parse_observe().map(Declaration::Observe),
2107
2108            // ── §λ-L-E Fase 3 — Control cognitivo ───────────────
2109            TokenType::Reconcile => self.parse_reconcile().map(Declaration::Reconcile),
2110            TokenType::Lease => self.parse_lease().map(Declaration::Lease),
2111            TokenType::Ensemble => self.parse_ensemble().map(Declaration::Ensemble),
2112
2113            // ── §λ-L-E Fase 4 — Topology + π-calculus sessions ─
2114            TokenType::Session => self.parse_session_definition().map(Declaration::Session),
2115            TokenType::Topology => self.parse_topology().map(Declaration::Topology),
2116
2117            // ── §Fase 41.b — typed WebSocket transport ─────────
2118            TokenType::Socket => self.parse_socket().map(Declaration::Socket),
2119
2120            // ── §Fase 80.b — outbound vendor connection ─────────
2121            TokenType::Upstream => self.parse_upstream().map(Declaration::Upstream),
2122
2123            // ── §Fase 80.g — the voice-agent simplicity layer ───
2124            TokenType::Voice => self.parse_voice().map(Declaration::Voice),
2125
2126            // ── §Fase 83.a — the named origin-policy declaration ─
2127            TokenType::Cors => self.parse_cors().map(Declaration::Cors),
2128
2129            // ── §Fase 85.a — the named result-memoization policy ─
2130            TokenType::Cache => self.parse_cache().map(Declaration::Cache),
2131            TokenType::Document => self.parse_document().map(Declaration::Document),
2132
2133            // ── §Fase 105 — Governed CRM Delivery ─
2134            TokenType::Deliver => self.parse_deliver().map(Declaration::Deliver),
2135            TokenType::Notify => self.parse_notify().map(Declaration::Notify),
2136
2137            // ── §Fase 87.a — the long-horizon autonomous research primitive ─
2138            TokenType::Savant => self.parse_savant().map(Declaration::Savant),
2139
2140            // ── §Fase 87.d — the dynamic tool-synthesis policy ──────────────
2141            TokenType::Synth => self.parse_synth().map(Declaration::Synth),
2142
2143            // ── §Fase 88.a — the authorization-scope policy declaration ─────
2144            TokenType::Scope => self.parse_scope().map(Declaration::Scope),
2145
2146            // ── §Fase 92.a — the ephemeral-credential contract ──────────────
2147            TokenType::Credential => self.parse_credential().map(Declaration::Credential),
2148
2149            // ── §Fase 51.c.2 — Pauli-sum observable ────────────
2150            TokenType::Observable => self.parse_observable().map(Declaration::Observable),
2151
2152            // ── §Fase 69.a — Advantage Witness ──────────────────
2153            TokenType::Witness => self.parse_witness().map(Declaration::Witness),
2154
2155            // ── §λ-L-E Fase 5 — Cognitive immune system ─────────
2156            TokenType::Immune => self.parse_immune().map(Declaration::Immune),
2157            TokenType::Reflex => self.parse_reflex().map(Declaration::Reflex),
2158            TokenType::Heal => self.parse_heal().map(Declaration::Heal),
2159
2160            // ── §λ-L-E Fase 9 — UI cognitiva ────────────────────
2161            TokenType::Component => self.parse_component().map(Declaration::Component),
2162            TokenType::View => self.parse_view().map(Declaration::View),
2163
2164            // ── §λ-L-E Fase 13 — Mobile typed channels ──────────
2165            TokenType::Channel => self.parse_channel().map(Declaration::Channel),
2166
2167            // ── Tier 3+ structural fallback ─────────────────────
2168            // Store operations: keyword target { ... } or keyword target ...
2169            TokenType::Ingest
2170            | TokenType::Persist
2171            | TokenType::Retrieve
2172            | TokenType::Mutate
2173            | TokenType::Purge
2174            | TokenType::Transact => self.parse_generic_declaration(),
2175
2176            // MCP declaration
2177            TokenType::Mcp => self.parse_generic_declaration(),
2178
2179            _ => {
2180                // §Fase 28.e — append "Did you mean X?" hint when the
2181                // unknown token looks like a typo'd top-level keyword
2182                // (Levenshtein ≤ 2). D3, D11 ratified 2026-05-10.
2183                let hint = crate::smart_suggest::suggest_for(
2184                    &tok.value,
2185                    crate::smart_suggest::TOP_LEVEL_KEYWORD_NAMES,
2186                );
2187                let base = format!(
2188                    "Unexpected token at top level: '{}' — expected declaration \
2189                     (persona, context, anchor, flow, run, ...)",
2190                    tok.value
2191                );
2192                let message = if hint.is_empty() {
2193                    base
2194                } else {
2195                    format!("{base}. {hint}")
2196                };
2197                Err(ParseError {
2198                    message,
2199                    line: tok.line,
2200                    column: tok.column,
2201                    ..Default::default()
2202                })
2203            }
2204        }
2205    }
2206
2207    // ── IMPORT ───────────────────────────────────────────────────
2208
2209    fn parse_import(&mut self) -> Result<ImportNode, ParseError> {
2210        let tok = self.consume(TokenType::Import)?;
2211        let loc = self.loc_of(&tok);
2212
2213        let mut path_parts = Vec::new();
2214
2215        // Optional @ scope
2216        if self.check(TokenType::At) {
2217            self.advance();
2218            let first = self.consume(TokenType::Identifier)?;
2219            path_parts.push(format!("@{}", first.value));
2220        } else {
2221            let first = self.consume(TokenType::Identifier)?;
2222            path_parts.push(first.value);
2223        }
2224
2225        while self.check(TokenType::Dot) {
2226            self.advance();
2227            if self.check(TokenType::LBrace) {
2228                break;
2229            }
2230            let part = self.consume(TokenType::Identifier)?;
2231            path_parts.push(part.value);
2232        }
2233
2234        let mut names = Vec::new();
2235        if self.check(TokenType::LBrace) {
2236            self.advance();
2237            names = self.parse_identifier_list()?;
2238            self.consume(TokenType::RBrace)?;
2239        }
2240
2241        // ── §Fase 115.c — the `@allow_downgrade` ECC valve ───────────────
2242        //
2243        // `import a.b.{X} @allow_downgrade` acknowledges an epistemic
2244        // downgrade across this edge (see `epistemic_compat.rs`). The
2245        // annotation position is unambiguous: no top-level declaration
2246        // begins with `@`, so an `@` here belongs to this import — and an
2247        // unknown annotation is refused with the fix in the message
2248        // rather than surfacing later as an opaque parse error.
2249        let mut allow_downgrade = false;
2250        if self.check(TokenType::At) {
2251            let at_tok = self.current().clone();
2252            self.advance();
2253            let ident = self.consume(TokenType::Identifier)?;
2254            if ident.value == "allow_downgrade" {
2255                allow_downgrade = true;
2256            } else {
2257                return Err(ParseError {
2258                    message: format!(
2259                        "unknown import annotation '@{}' — the only import annotation is \
2260                         `@allow_downgrade` (the §115 epistemic-downgrade acknowledgment).",
2261                        ident.value
2262                    ),
2263                    line: at_tok.line,
2264                    column: at_tok.column,
2265                    ..Default::default()
2266                });
2267            }
2268        }
2269
2270        // ── §Fase 111 — `apx` is RETRACTED ───────────────────────────────
2271        //
2272        // `import X with apx { … }` used to parse and then call
2273        // `skip_braced_block()` — the policy was consumed and thrown on the
2274        // floor. It never reached the AST, let alone the IR. In `axon-rs` the
2275        // string "apx" occurred only inside comments: there is no APX crate,
2276        // no binary, no MEC/PCC dependency verification, no EPR ranking, no
2277        // quarantine and no compliance gate. The public README advertised all
2278        // five.
2279        //
2280        // A dependency policy that silently evaporates is the worst possible
2281        // shape for this particular promise: the adopter believes their supply
2282        // chain is being verified, which is exactly the belief that stops them
2283        // from verifying it themselves. Refuse, loudly.
2284        let next_is_apx = self
2285            .tokens
2286            .get(self.pos + 1)
2287            .map(|t| t.value == "apx")
2288            .unwrap_or(false);
2289        if self.current().value == "with" && next_is_apx {
2290            let tok = self.current().clone();
2291            return Err(ParseError {
2292                message: "`import … with apx { … }` is RETRACTED (§111). The apx policy block was \
2293                          parsed and silently DISCARDED — it never reached the IR, and no epistemic \
2294                          dependency manager exists: no MEC/PCC verification, no EPR ranking, no \
2295                          quarantine, no compliance gate. Declaring it verified nothing while \
2296                          implying your supply chain was checked. Remove the `with apx { … }` \
2297                          clause; the plain `import` resolves through the §115 Epistemic Module \
2298                          System."
2299                    .to_string(),
2300                line: tok.line,
2301                column: tok.column,
2302                ..Default::default()
2303            });
2304        }
2305
2306        Ok(ImportNode {
2307            module_path: path_parts,
2308            names,
2309            allow_downgrade,
2310            loc,
2311            leading_trivia: Vec::new(),
2312            trailing_trivia: Vec::new(),
2313        })
2314    }
2315
2316    // ── PERSONA ──────────────────────────────────────────────────
2317
2318    fn parse_persona(&mut self) -> Result<PersonaDefinition, ParseError> {
2319        let tok = self.consume(TokenType::Persona)?;
2320        let loc = self.loc_of(&tok);
2321        let name = self.consume(TokenType::Identifier)?.value;
2322        self.consume(TokenType::LBrace)?;
2323
2324        let mut node = PersonaDefinition {
2325            name,
2326            domain: Vec::new(),
2327            tone: String::new(),
2328            confidence_threshold: None,
2329            cite_sources: None,
2330            refuse_if: Vec::new(),
2331            language: String::new(),
2332            description: String::new(),
2333            loc,
2334            leading_trivia: Vec::new(),
2335            trailing_trivia: Vec::new(),
2336        };
2337
2338        while !self.check(TokenType::RBrace) {
2339            let field_name = self.current().value.clone();
2340            self.advance();
2341            self.consume(TokenType::Colon)?;
2342
2343            match field_name.as_str() {
2344                "domain" => node.domain = self.parse_string_list()?,
2345                "tone" => node.tone = self.consume_any_ident_or_kw()?.value,
2346                "confidence_threshold" => node.confidence_threshold = Some(self.consume_number()?),
2347                "cite_sources" => node.cite_sources = Some(self.parse_bool()?),
2348                "refuse_if" => node.refuse_if = self.parse_bracketed_identifiers()?,
2349                "language" => node.language = self.consume(TokenType::StringLit)?.value,
2350                "description" => node.description = self.consume(TokenType::StringLit)?.value,
2351                _ => self.skip_value(),
2352            }
2353        }
2354        self.consume(TokenType::RBrace)?;
2355        Ok(node)
2356    }
2357
2358    // ── CONTEXT ──────────────────────────────────────────────────
2359
2360    fn parse_context(&mut self) -> Result<ContextDefinition, ParseError> {
2361        let tok = self.consume(TokenType::Context)?;
2362        let loc = self.loc_of(&tok);
2363        let name = self.consume(TokenType::Identifier)?.value;
2364        self.consume(TokenType::LBrace)?;
2365
2366        let mut node = ContextDefinition {
2367            name,
2368            memory_scope: String::new(),
2369            language: String::new(),
2370            depth: String::new(),
2371            max_tokens: None,
2372            temperature: None,
2373            cite_sources: None,
2374            now_tz: None,
2375            loc,
2376            leading_trivia: Vec::new(),
2377            trailing_trivia: Vec::new(),
2378        };
2379
2380        while !self.check(TokenType::RBrace) {
2381            let field_name = self.current().value.clone();
2382            self.advance();
2383            self.consume(TokenType::Colon)?;
2384
2385            match field_name.as_str() {
2386                "memory" => node.memory_scope = self.consume_any_ident_or_kw()?.value,
2387                "language" => node.language = self.consume(TokenType::StringLit)?.value,
2388                "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
2389                // §Fase 91.a — the frame's cognitive timezone (IANA string).
2390                "now" => node.now_tz = Some(self.consume(TokenType::StringLit)?.value),
2391                "max_tokens" => {
2392                    node.max_tokens = Some(
2393                        self.consume(TokenType::Integer)?
2394                            .value
2395                            .parse::<i64>()
2396                            .unwrap_or(0),
2397                    )
2398                }
2399                "temperature" => node.temperature = Some(self.consume_number()?),
2400                "cite_sources" => node.cite_sources = Some(self.parse_bool()?),
2401                _ => self.skip_value(),
2402            }
2403        }
2404        self.consume(TokenType::RBrace)?;
2405        Ok(node)
2406    }
2407
2408    // ── ANCHOR ───────────────────────────────────────────────────
2409
2410    fn parse_anchor(&mut self) -> Result<AnchorConstraint, ParseError> {
2411        let tok = self.consume(TokenType::Anchor)?;
2412        let loc = self.loc_of(&tok);
2413        let name = self.consume(TokenType::Identifier)?.value;
2414        self.consume(TokenType::LBrace)?;
2415
2416        let mut node = AnchorConstraint {
2417            name,
2418            require: String::new(),
2419            reject: Vec::new(),
2420            enforce: String::new(),
2421            description: String::new(),
2422            confidence_floor: None,
2423            unknown_response: String::new(),
2424            on_violation: String::new(),
2425            on_violation_target: String::new(),
2426            loc,
2427            leading_trivia: Vec::new(),
2428            trailing_trivia: Vec::new(),
2429        };
2430
2431        while !self.check(TokenType::RBrace) {
2432            let field_name = self.current().value.clone();
2433            self.advance();
2434            self.consume(TokenType::Colon)?;
2435
2436            match field_name.as_str() {
2437                "require" => node.require = self.consume_any_ident_or_kw()?.value,
2438                "description" => node.description = self.consume(TokenType::StringLit)?.value,
2439                "reject" => node.reject = self.parse_bracketed_identifiers()?,
2440                "enforce" => node.enforce = self.consume_any_ident_or_kw()?.value,
2441                "confidence_floor" => node.confidence_floor = Some(self.consume_number()?),
2442                "unknown_response" => {
2443                    node.unknown_response = self.consume(TokenType::StringLit)?.value
2444                }
2445                "on_violation" => {
2446                    // Parse: raise ErrorName | fallback(...) | identifier
2447                    let action = self.consume_any_ident_or_kw()?.value;
2448                    node.on_violation = action.clone();
2449                    if action == "raise" || action == "fallback" {
2450                        node.on_violation_target = self.consume_any_ident_or_kw()?.value;
2451                    }
2452                }
2453                _ => self.skip_value(),
2454            }
2455        }
2456        self.consume(TokenType::RBrace)?;
2457        Ok(node)
2458    }
2459
2460    // ── MEMORY ───────────────────────────────────────────────────
2461
2462    fn parse_memory(&mut self) -> Result<MemoryDefinition, ParseError> {
2463        let tok = self.consume(TokenType::Memory)?;
2464        let loc = self.loc_of(&tok);
2465        let name = self.consume(TokenType::Identifier)?.value;
2466        self.consume(TokenType::LBrace)?;
2467
2468        let mut node = MemoryDefinition {
2469            name,
2470            store: String::new(),
2471            backend: String::new(),
2472            retrieval: String::new(),
2473            decay: String::new(),
2474            loc,
2475            leading_trivia: Vec::new(),
2476            trailing_trivia: Vec::new(),
2477        };
2478
2479        while !self.check(TokenType::RBrace) {
2480            let field_name = self.current().value.clone();
2481            self.advance();
2482            self.consume(TokenType::Colon)?;
2483
2484            match field_name.as_str() {
2485                "store" => node.store = self.consume_any_ident_or_kw()?.value,
2486                "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
2487                "retrieval" => node.retrieval = self.consume_any_ident_or_kw()?.value,
2488                "decay" => {
2489                    if self.check(TokenType::Duration) {
2490                        node.decay = self.advance().value.clone();
2491                    } else {
2492                        node.decay = self.consume_any_ident_or_kw()?.value;
2493                    }
2494                }
2495                _ => self.skip_value(),
2496            }
2497        }
2498        self.consume(TokenType::RBrace)?;
2499        Ok(node)
2500    }
2501
2502    // ── TOOL ─────────────────────────────────────────────────────
2503
2504    fn parse_tool(&mut self) -> Result<ToolDefinition, ParseError> {
2505        let tok = self.consume(TokenType::Tool)?;
2506        let loc = self.loc_of(&tok);
2507        let name = self.consume(TokenType::Identifier)?.value;
2508        self.consume(TokenType::LBrace)?;
2509
2510        let mut node = ToolDefinition {
2511            name,
2512            provider: String::new(),
2513            max_results: None,
2514            filter_expr: String::new(),
2515            timeout: String::new(),
2516            runtime: String::new(),
2517            resource_ref: String::new(),
2518            sandbox: None,
2519            effects: None,
2520            parameters: Vec::new(),
2521            output_type: None,
2522            requires: Vec::new(),
2523            secret: String::new(),
2524            secret_partition: String::new(),
2525            target: None,
2526            risk: None,
2527            argv: Vec::new(),
2528            cache: String::new(),
2529            scrape: None,
2530            loc,
2531            leading_trivia: Vec::new(),
2532            trailing_trivia: Vec::new(),
2533        };
2534
2535        // §Fase 84.b/D84.13 — unknown fields are recorded (not silently
2536        // skipped) so a `target:`-bound technician tool can HARD-ERROR on one
2537        // (a typo'd safety field must never quietly disable a guard), while a
2538        // legacy schema-less tool keeps its lenient record-and-skip (zero
2539        // regression). The decision is deferred to after the block is parsed,
2540        // since `target:` may appear after the unknown field.
2541        let mut unknown_fields: Vec<(String, u32, u32)> = Vec::new();
2542
2543        while !self.check(TokenType::RBrace) {
2544            let field_tok = self.current().clone();
2545            let field_name = field_tok.value.clone();
2546            self.advance();
2547            self.consume(TokenType::Colon)?;
2548
2549            match field_name.as_str() {
2550                "provider" => node.provider = self.consume_any_ident_or_kw()?.value,
2551                "max_results" => {
2552                    node.max_results = Some(
2553                        self.consume(TokenType::Integer)?
2554                            .value
2555                            .parse::<i64>()
2556                            .unwrap_or(0),
2557                    )
2558                }
2559                "filter" => node.filter_expr = self.parse_filter_expression()?,
2560                "timeout" => node.timeout = self.consume(TokenType::Duration)?.value,
2561                "runtime" => node.runtime = self.consume_any_ident_or_kw()?.value,
2562                // §Fase 114.c — the `resource` this tool's channel runs on. The
2563                // channel's address, concurrency and lifecycle come from it;
2564                // `runtime:` then names the path within the channel.
2565                "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
2566                "sandbox" => node.sandbox = Some(self.parse_bool()?),
2567                "effects" => node.effects = Some(self.parse_effect_row()?),
2568                // §Fase 58.a — the tool's typed input schema + output type.
2569                "parameters" => node.parameters = self.parse_tool_param_schema()?,
2570                "output_type" => node.output_type = Some(self.parse_output_type_string()?),
2571                // §Fase 116.a (D116.9) — the tool's required authorization
2572                // scopes: bare dot-separated capability slugs, the EXACT
2573                // grammar + charset of `credential.grants` (§92) so the two
2574                // vocabularies are one. `requires: [w_organization_social,
2575                // video.publish]`. Subset coverage is `axon-T956`.
2576                "requires" => {
2577                    let bracket_tok = self.current().clone();
2578                    let items = self.parse_bracketed_dot_identifiers()?;
2579                    for slug in &items {
2580                        if !is_valid_capability_slug(slug) {
2581                            return Err(ParseError {
2582                                message: format!(
2583                                    "Invalid capability slug '{slug}' in tool '{}' \
2584                                     `requires:`. Scope slugs must match \
2585                                     ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — the same \
2586                                     grammar as `credential.grants`. Examples: \
2587                                     `w_organization_social`, `video.publish`.",
2588                                    node.name
2589                                ),
2590                                line: bracket_tok.line,
2591                                column: bracket_tok.column,
2592                                ..Default::default()
2593                            });
2594                        }
2595                    }
2596                    node.requires = items;
2597                }
2598                // §Fase 94.c — the per-tenant secret KEY injected at
2599                // dispatch (`rotation_without_revelation`). Key shape +
2600                // technician exclusion are `axon-T902` (type-checker).
2601                "secret" => node.secret = self.parse_dotted_identifier()?,
2602                // §Fase 95.a — `secret_partition:` names one of this tool's
2603                // own `parameters:` (a bare identifier, NOT dotted — it is a
2604                // parameter reference, not a key). Its runtime value becomes
2605                // a single appended key segment at dispatch. The membership +
2606                // `String`-type + technician laws are `axon-T903`.
2607                "secret_partition" => {
2608                    node.secret_partition = self.consume_any_ident_or_kw()?.value
2609                }
2610                // §Fase 84.b — Remote Hands technician fields.
2611                "target" => node.target = Some(self.consume_any_ident_or_kw()?.value),
2612                "risk" => node.risk = Some(self.consume_any_ident_or_kw()?.value),
2613                // The argv template: a bracketed list of quoted elements
2614                // (`argv: ["ping", "-c", "${count}", "${host}"]`). Reuses the
2615                // CORS list helper (tolerant of `[]` and a trailing comma).
2616                "argv" => node.argv = self.parse_bracketed_strings()?,
2617                // §Fase 85.b — the tool's result-memoization policy reference
2618                // (a declared `cache` name, or the `none` opt-out sentinel).
2619                "cache" => node.cache = self.consume_any_ident_or_kw()?.value,
2620                // §Fase 98.b — the closed-catalog web-acquisition config
2621                // block. `scrape: { engine: …, extract: […], … }`.
2622                "scrape" => node.scrape = Some(self.parse_scrape_spec()?),
2623                _ => {
2624                    unknown_fields.push((field_name, field_tok.line, field_tok.column));
2625                    self.skip_value();
2626                }
2627            }
2628        }
2629        self.consume(TokenType::RBrace)?;
2630
2631        // §Fase 84.b/D84.13 — a `target:`-bound tool opts into strict field
2632        // checking. An unknown field on it is a parse error, mirroring the §83
2633        // `cors`/`voice` closed-catalog discipline — but scoped to the
2634        // technician surface so ordinary tools are untouched.
2635        // §Fase 98.b (D98.2) — a `scrape:`-bearing web-acquisition tool opts
2636        // into the same strictness: a typo'd safety field (e.g. a mis-spelled
2637        // `respect_robots`) must never quietly disable a guard.
2638        if node.target.is_some() || node.scrape.is_some() {
2639            if let Some((field_name, line, column)) = unknown_fields.into_iter().next() {
2640                let (surface, valid) = if node.target.is_some() {
2641                    (
2642                        "technician tool (§Fase 84 D84.13)",
2643                        "provider, parameters, output_type, timeout, effects, target, risk, argv",
2644                    )
2645                } else {
2646                    (
2647                        "web-acquisition tool (§Fase 98 D98.2)",
2648                        "provider, parameters, output_type, timeout, effects, secret, \
2649                         secret_partition, cache, scrape",
2650                    )
2651                };
2652                return Err(ParseError {
2653                    message: format!(
2654                        "unknown field `{field_name}` in {surface} `{}` — this tool uses \
2655                         strict field checking; valid fields: {valid}",
2656                        node.name
2657                    ),
2658                    line,
2659                    column,
2660                    ..Default::default()
2661                });
2662            }
2663        }
2664        Ok(node)
2665    }
2666
2667    /// §Fase 98.b — parse the closed-catalog `scrape: { … }` web-acquisition
2668    /// config sub-block. Every field is optional; an unknown field is a hard
2669    /// parse error (the §83 `cors` closed-catalog discipline). Mirrors the
2670    /// field grammar of `parse_tool` for the scrape-specific keys.
2671    fn parse_scrape_spec(&mut self) -> Result<crate::ast::ScrapeSpec, ParseError> {
2672        let open = self.consume(TokenType::LBrace)?;
2673        let loc = self.loc_of(&open);
2674        let mut spec = crate::ast::ScrapeSpec {
2675            loc,
2676            ..Default::default()
2677        };
2678        while !self.check(TokenType::RBrace) {
2679            let field_tok = self.current().clone();
2680            let field_name = field_tok.value.clone();
2681            self.advance();
2682            self.consume(TokenType::Colon)?;
2683            match field_name.as_str() {
2684                "engine" => spec.engine = Some(self.consume_any_ident_or_kw()?.value),
2685                "impersonate" => spec.impersonate = Some(self.consume_any_ident_or_kw()?.value),
2686                "render_wait" => spec.render_wait = Some(self.consume(TokenType::Duration)?.value),
2687                "proxy" => spec.proxy = self.parse_dotted_identifier()?,
2688                "respect_robots" => spec.respect_robots = Some(self.parse_bool()?),
2689                "extract" => spec.extract = self.parse_bracketed_strings()?,
2690                "adaptive" => spec.adaptive = Some(self.parse_bool()?),
2691                "similarity_floor" => spec.similarity_floor = self.parse_optional_float(),
2692                "follow" => spec.follow = self.consume(TokenType::StringLit)?.value,
2693                "max_depth" => {
2694                    spec.max_depth =
2695                        Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2696                }
2697                "max_pages" => {
2698                    spec.max_pages =
2699                        Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2700                }
2701                "concurrency" => {
2702                    spec.concurrency =
2703                        Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2704                }
2705                "politeness" => spec.politeness = self.consume_any_ident_or_kw()?.value,
2706                "checkpoint" => spec.checkpoint = self.consume_any_ident_or_kw()?.value,
2707                other => {
2708                    return Err(self.error(&format!(
2709                        "unknown scrape field `{other}` — the `scrape: {{ … }}` block is a \
2710                         closed catalog (§Fase 98 D98.2); valid fields: engine, impersonate, \
2711                         render_wait, proxy, respect_robots, extract, adaptive, \
2712                         similarity_floor, follow, max_depth, max_pages, concurrency, \
2713                         politeness, checkpoint"
2714                    )));
2715                }
2716            }
2717        }
2718        self.consume(TokenType::RBrace)?;
2719        Ok(spec)
2720    }
2721
2722    /// §Fase 58.a — parse a tool's INPUT SCHEMA: a brace-delimited list of
2723    /// `name: Type` parameters (`parameters: { query: String, max_results: Int }`).
2724    /// Reuses the flow-parameter shape (`Parameter`), so the same `TypeExpr`
2725    /// grammar — generics like `List<T>`, `?`-optionals — applies. A trailing
2726    /// comma is tolerated; an empty `{}` yields no parameters.
2727    fn parse_tool_param_schema(&mut self) -> Result<Vec<Parameter>, ParseError> {
2728        self.consume(TokenType::LBrace)?;
2729        let mut params = Vec::new();
2730        while !self.check(TokenType::RBrace) {
2731            // Accept a keyword-as-name (`filter`, `type`, `domain`, …) — real
2732            // adopter tool schemas use such parameter names; the `:` after it
2733            // disambiguates.
2734            let name = self.consume_any_ident_or_kw()?;
2735            let ploc = self.loc_of(&name);
2736            self.consume(TokenType::Colon)?;
2737            let type_expr = self.parse_type_expr()?;
2738            params.push(Parameter {
2739                name: name.value,
2740                type_expr,
2741                loc: ploc,
2742            });
2743            if self.check(TokenType::Comma) {
2744                self.advance();
2745            } else {
2746                break;
2747            }
2748        }
2749        self.consume(TokenType::RBrace)?;
2750        Ok(params)
2751    }
2752
2753    fn parse_filter_expression(&mut self) -> Result<String, ParseError> {
2754        let name = self.consume_any_ident_or_kw()?.value;
2755        if self.check(TokenType::LParen) {
2756            self.advance();
2757            let mut parts = vec![name, "(".to_string()];
2758            while !self.check(TokenType::RParen) {
2759                parts.push(self.advance().value.clone());
2760            }
2761            self.consume(TokenType::RParen)?;
2762            parts.push(")".to_string());
2763            Ok(parts.join(""))
2764        } else {
2765            Ok(name)
2766        }
2767    }
2768
2769    fn parse_effect_row(&mut self) -> Result<EffectRow, ParseError> {
2770        let tok = self.consume(TokenType::Lt)?;
2771        let loc = self.loc_of(&tok);
2772        let mut effects = Vec::new();
2773        let mut epistemic_level = String::new();
2774
2775        while !self.check(TokenType::Gt) {
2776            let name = self.consume_any_ident_or_kw()?.value;
2777            if self.check(TokenType::Colon) {
2778                self.advance();
2779                // Fase 11.c / 11.e — qualifiers can be compound slugs
2780                // from a closed catalogue:
2781                //
2782                //   * dot-separated  — `legal:HIPAA.164_502`,
2783                //                       `legal:GDPR.Art6.Consent`,
2784                //                       `legal:PCI_DSS.v4_Req3`
2785                //   * colon-separated — `ots:transform:mulaw8:pcm16`,
2786                //                       `ots:backend:native`
2787                //   * mixed           — supported by the same loop.
2788                //
2789                // The lexer fragments dotted slugs across IDENT /
2790                // INTEGER tokens (e.g., `164_502` lexes as INTEGER
2791                // `164` + IDENT `_502` because `_` starts a fresh
2792                // identifier); we recombine here using source-column
2793                // adjacency so the type checker sees the catalog
2794                // string verbatim.
2795                let level = self.parse_qualifier_value()?;
2796                if name == "epistemic" {
2797                    epistemic_level = level;
2798                } else {
2799                    effects.push(format!("{name}:{level}"));
2800                }
2801            } else {
2802                effects.push(name);
2803            }
2804            if self.check(TokenType::Comma) {
2805                self.advance();
2806            }
2807        }
2808        self.consume(TokenType::Gt)?;
2809
2810        Ok(EffectRow {
2811            effects,
2812            epistemic_level,
2813            loc,
2814        })
2815    }
2816
2817    /// Parse a compound qualifier value following an effect's first
2818    /// colon — supports both dot-separated (`HIPAA.164_502`) and
2819    /// colon-separated (`transform:mulaw8:pcm16`) catalogue slugs, as
2820    /// well as mixed forms.
2821    ///
2822    /// The grammar is: `segment ((`.` | `:`) segment)*` where a
2823    /// segment is a contiguous run of IDENT / INTEGER tokens (see
2824    /// [`Self::consume_dotted_slug_segment`]).
2825    fn parse_qualifier_value(&mut self) -> Result<String, ParseError> {
2826        let mut buf = self.consume_dotted_slug_segment()?;
2827        loop {
2828            let sep = if self.check(TokenType::Dot) {
2829                '.'
2830            } else if self.check(TokenType::Colon) {
2831                ':'
2832            } else {
2833                break;
2834            };
2835            self.advance();
2836            let part = self.consume_dotted_slug_segment()?;
2837            buf.push(sep);
2838            buf.push_str(&part);
2839        }
2840        Ok(buf)
2841    }
2842
2843    /// Consume a contiguous run of IDENT / INTEGER / keyword-ident
2844    /// tokens whose source positions are adjacent (no whitespace
2845    /// between them), concatenating their text into a single segment.
2846    ///
2847    /// Needed for closed-catalogue qualifier slugs whose segment
2848    /// mixes digits and identifier characters — e.g. `HIPAA.164_502`
2849    /// lexes as INTEGER `164` + IDENT `_502` because `_` starts a
2850    /// fresh identifier; the catalog value is the concatenation
2851    /// `164_502`. Adjacency is determined by matching
2852    /// `(line, column + len)` of the previous token against the next
2853    /// token's start position.
2854    fn consume_dotted_slug_segment(&mut self) -> Result<String, ParseError> {
2855        let first = self.consume_any_ident_or_kw()?;
2856        let mut buf = first.value.clone();
2857        let mut next_line = first.line;
2858        let mut next_col = first.column + first.value.chars().count() as u32;
2859        loop {
2860            let cur = self.current();
2861            let is_segment_token = matches!(cur.ttype, TokenType::Identifier | TokenType::Integer,);
2862            if !is_segment_token {
2863                break;
2864            }
2865            if cur.line != next_line || cur.column != next_col {
2866                break;
2867            }
2868            buf.push_str(&cur.value);
2869            next_col = cur.column + cur.value.chars().count() as u32;
2870            next_line = cur.line;
2871            self.pos += 1;
2872        }
2873        Ok(buf)
2874    }
2875
2876    // ── TYPE ─────────────────────────────────────────────────────
2877
2878    fn parse_type_def(&mut self) -> Result<TypeDefinition, ParseError> {
2879        let tok = self.consume(TokenType::Type)?;
2880        let loc = self.loc_of(&tok);
2881        let name = self.consume(TokenType::Identifier)?.value;
2882
2883        let mut node = TypeDefinition {
2884            name,
2885            fields: Vec::new(),
2886            range_constraint: None,
2887            where_clause: None,
2888            compliance: Vec::new(),
2889            loc: loc.clone(),
2890            leading_trivia: Vec::new(),
2891            trailing_trivia: Vec::new(),
2892        };
2893
2894        // Optional range: (0.0..1.0)
2895        if self.check(TokenType::LParen) {
2896            self.advance();
2897            let min_val = self.consume_number()?;
2898            self.consume(TokenType::DotDot)?;
2899            let max_val = self.consume_number()?;
2900            self.consume(TokenType::RParen)?;
2901            node.range_constraint = Some(RangeConstraint {
2902                min_value: min_val,
2903                max_value: max_val,
2904                loc: loc.clone(),
2905            });
2906        }
2907
2908        // Optional where clause
2909        if self.check(TokenType::Where) {
2910            self.advance();
2911            let mut expr_parts = Vec::new();
2912            while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
2913                if self.check(TokenType::Eof) {
2914                    break;
2915                }
2916                expr_parts.push(self.advance().value.clone());
2917            }
2918            node.where_clause = Some(WhereClause {
2919                expression: expr_parts.join(" "),
2920                loc: loc.clone(),
2921            });
2922        }
2923
2924        // Optional ESK Fase 6.1 — `compliance [HIPAA, ...]` prefix modifier
2925        // between `type Name` / `range` / `where` and the body `{`.
2926        if self.check(TokenType::Identifier) && self.current().value == "compliance" {
2927            self.advance();
2928            node.compliance = self.parse_bracketed_identifiers()?;
2929        }
2930
2931        // Optional body: { field: Type, ... }
2932        if self.check(TokenType::LBrace) {
2933            self.advance();
2934            while !self.check(TokenType::RBrace) {
2935                let field_name = self.consume(TokenType::Identifier)?;
2936                let field_loc = self.loc_of(&field_name);
2937                self.consume(TokenType::Colon)?;
2938                let type_expr = self.parse_type_expr()?;
2939                node.fields.push(TypeField {
2940                    name: field_name.value,
2941                    type_expr,
2942                    loc: field_loc,
2943                });
2944                if self.check(TokenType::Comma) {
2945                    self.advance();
2946                }
2947            }
2948            self.consume(TokenType::RBrace)?;
2949        }
2950
2951        Ok(node)
2952    }
2953
2954    fn parse_type_expr(&mut self) -> Result<TypeExpr, ParseError> {
2955        let name_tok = self.consume(TokenType::Identifier)?;
2956        let loc = self.loc_of(&name_tok);
2957        let mut generic_param = String::new();
2958        let mut optional = false;
2959
2960        if self.check(TokenType::Lt) {
2961            self.advance();
2962            // §Fase 39.a — recursive: the generic param can itself be a
2963            // nested type expression. `FlowEnvelope<List<TenantRecord>>`
2964            // parses as outer=FlowEnvelope, inner=List<TenantRecord>.
2965            // Pre-39.a the inner had to be a single Identifier; nested
2966            // generics like the canonical FlowEnvelope<T> wrapper
2967            // required this lift. Backwards-compat preserved for
2968            // single-level generics like `Stream<Token>` and
2969            // `List<T>` — the recursion lands once and returns the
2970            // same flat string the v1.x parser produced.
2971            let inner = self.parse_type_expr()?;
2972            generic_param = if inner.generic_param.is_empty() {
2973                inner.name
2974            } else {
2975                format!("{}<{}>", inner.name, inner.generic_param)
2976            };
2977            self.consume(TokenType::Gt)?;
2978        }
2979        // §Fase 51.c.3 — bracket type parameters for the continuous-carrier
2980        // grammar: `SymbolicPtr[Tensor[Float32]]`, `DensityMatrix[1024]`. The
2981        // param is either a nested type expression OR a numeric dimension.
2982        if self.check(TokenType::LBracket) {
2983            self.advance();
2984            if matches!(self.current().ttype, TokenType::Integer | TokenType::Float) {
2985                generic_param = self.advance().value.clone();
2986            } else {
2987                let inner = self.parse_type_expr()?;
2988                generic_param = if inner.generic_param.is_empty() {
2989                    inner.name
2990                } else {
2991                    format!("{}[{}]", inner.name, inner.generic_param)
2992                };
2993            }
2994            self.consume(TokenType::RBracket)?;
2995        }
2996        if self.check(TokenType::Question) {
2997            self.advance();
2998            optional = true;
2999        }
3000
3001        Ok(TypeExpr {
3002            name: name_tok.value,
3003            generic_param,
3004            optional,
3005            loc,
3006        })
3007    }
3008
3009    /// Parse a type expression in a context where the AST stores the
3010    /// shape as a flat string (step / reason / forge / ots-apply
3011    /// productions). Mirrors Python `_parse_output_type_string`.
3012    ///
3013    /// Accepts:
3014    /// - `Identifier`        → `"Identifier"`
3015    /// - `Stream<String>`    → `"Stream<String>"`
3016    /// - `Optional?`         → `"Optional?"`
3017    /// - `Stream<String>?`   → `"Stream<String>?"`
3018    ///
3019    /// **Why this exists** — pre-fix, the step parser called
3020    /// `consume(TokenType::Identifier)?.value` which captured only
3021    /// the head identifier and left `< … >` unconsumed. For
3022    /// `output: Stream<Token>`, this produced `output_type =
3023    /// "Stream"`, and downstream `flow_has_stream_output`'s
3024    /// `starts_with("Stream<") && ends_with('>')` predicate then
3025    /// returned false → `implicit_transport == "json"` → the
3026    /// dynamic-route fallback in `axon-rs` served JSON instead of
3027    /// SSE even when the adopter's source canonically declared the
3028    /// algebraic stream effect. Surfaced 2026-05-12 by adopter
3029    /// `docs/MIGRATION_TO_AXON.md` audit after the v1.23.0 wire-
3030    /// layer didn't honor the declarative effect. Python parser was
3031    /// fixed for the same gap 2026-05-09; this is the Rust cross-
3032    /// stack catch-up.
3033    fn parse_output_type_string(&mut self) -> Result<String, ParseError> {
3034        let expr = self.parse_type_expr()?;
3035        let mut s = expr.name;
3036        if !expr.generic_param.is_empty() {
3037            s.push('<');
3038            s.push_str(&expr.generic_param);
3039            s.push('>');
3040        }
3041        if expr.optional {
3042            s.push('?');
3043        }
3044        Ok(s)
3045    }
3046
3047    // ── FLOW ─────────────────────────────────────────────────────
3048
3049    fn parse_flow(&mut self) -> Result<FlowDefinition, ParseError> {
3050        let tok = self.consume(TokenType::Flow)?;
3051        let loc = self.loc_of(&tok);
3052        let name = self.consume(TokenType::Identifier)?.value;
3053
3054        self.consume(TokenType::LParen)?;
3055        let mut parameters = Vec::new();
3056        if !self.check(TokenType::RParen) {
3057            parameters = self.parse_param_list()?;
3058        }
3059        self.consume(TokenType::RParen)?;
3060
3061        let mut return_type = None;
3062        if self.check(TokenType::Arrow) {
3063            self.advance();
3064            return_type = Some(self.parse_type_expr()?);
3065        }
3066
3067        self.consume(TokenType::LBrace)?;
3068        let mut body = Vec::new();
3069        while !self.check(TokenType::RBrace) {
3070            body.push(self.parse_flow_step()?);
3071        }
3072        self.consume(TokenType::RBrace)?;
3073
3074        Ok(FlowDefinition {
3075            name,
3076            parameters,
3077            return_type,
3078            body,
3079            loc,
3080            leading_trivia: Vec::new(),
3081            trailing_trivia: Vec::new(),
3082        })
3083    }
3084
3085    fn parse_param_list(&mut self) -> Result<Vec<Parameter>, ParseError> {
3086        let mut params = Vec::new();
3087
3088        let name = self.consume(TokenType::Identifier)?;
3089        let ploc = self.loc_of(&name);
3090        self.consume(TokenType::Colon)?;
3091        let type_expr = self.parse_type_expr()?;
3092        params.push(Parameter {
3093            name: name.value,
3094            type_expr,
3095            loc: ploc,
3096        });
3097
3098        while self.check(TokenType::Comma) {
3099            self.advance();
3100            let name = self.consume(TokenType::Identifier)?;
3101            let ploc = self.loc_of(&name);
3102            self.consume(TokenType::Colon)?;
3103            let type_expr = self.parse_type_expr()?;
3104            params.push(Parameter {
3105                name: name.value,
3106                type_expr,
3107                loc: ploc,
3108            });
3109        }
3110        Ok(params)
3111    }
3112
3113    // ── FLOW STEP dispatch ───────────────────────────────────────
3114
3115    fn parse_flow_step(&mut self) -> Result<FlowStep, ParseError> {
3116        let tok = self.current().clone();
3117
3118        match tok.ttype {
3119            TokenType::Step => self.parse_step().map(FlowStep::Step),
3120            TokenType::If => self.parse_if().map(FlowStep::If),
3121            TokenType::For => self.parse_for_in().map(FlowStep::ForIn),
3122            TokenType::Let => self.parse_let().map(FlowStep::Let),
3123            TokenType::Return => self.parse_return().map(FlowStep::Return),
3124            TokenType::Break => self.parse_break().map(FlowStep::Break),
3125            TokenType::Continue => self.parse_continue().map(FlowStep::Continue),
3126            TokenType::Lambda => self.parse_lambda_data_apply().map(FlowStep::LambdaDataApply),
3127
3128            // ── Tier 2 flow steps (typed AST) ─────────────────────
3129            TokenType::Probe => self.parse_flow_step_simple("probe").map(|l| FlowStep::Probe(ProbeStep { target: l.1, loc: l.0 })),
3130            TokenType::Reason => self.parse_flow_step_simple("reason").map(|l| FlowStep::Reason(ReasonStep { strategy: String::new(), target: l.1, loc: l.0 })),
3131            TokenType::Validate => self.parse_flow_step_simple("validate").map(|l| FlowStep::Validate(ValidateStep { target: l.1, rule: String::new(), loc: l.0 })),
3132            TokenType::Refine => self.parse_flow_step_simple("refine").map(|l| FlowStep::Refine(RefineStep { target: l.1, strategy: String::new(), loc: l.0 })),
3133            TokenType::Weave => self.parse_weave_step(),
3134            TokenType::Use => self.parse_use_step(),
3135            TokenType::Remember => self.parse_remember_step(),
3136            TokenType::Recall => self.parse_recall_step(),
3137            TokenType::Par => self.parse_par_block().map(FlowStep::Par),
3138            TokenType::Hibernate => self.parse_hibernate_step(),
3139            TokenType::Deliberate => self.parse_block_step("deliberate").map(|l| FlowStep::Deliberate(DeliberateBlock { loc: l })),
3140            TokenType::Consensus => self.parse_block_step("consensus").map(|l| FlowStep::Consensus(ConsensusBlock { loc: l })),
3141            TokenType::Forge => self.parse_forge_step().map(FlowStep::Forge),
3142            TokenType::Focus => self.parse_focus_step(),
3143            TokenType::Grad => self.parse_grad_step(),
3144            TokenType::Associate => self.parse_associate_step(),
3145            TokenType::Aggregate => self.parse_aggregate_step(),
3146            TokenType::Explore => self.parse_explore_step(),
3147            TokenType::Ingest => self.parse_ingest_step(),
3148            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 })),
3149            // §Fase 111.e — `stream` parses its BODY. It used to go through
3150            // `parse_block_step`, whose entire job is `skip_braced_block()` —
3151            // the block's contents were thrown away at parse time, which is why
3152            // `run_stream` had nothing to run and "completed" with an empty
3153            // string while the README sold "Algebraic Effects and Free Monads".
3154            TokenType::Stream => self.parse_stream_block().map(FlowStep::Stream),
3155            TokenType::Navigate => self.parse_navigate_step(),
3156            TokenType::Drill => self.parse_drill_step(),
3157            TokenType::Trail => self.parse_flow_step_simple("trail").map(|l| FlowStep::Trail(TrailStep { navigate_ref: l.1, loc: l.0 })),
3158            TokenType::Corroborate => self.parse_corroborate_step(),
3159            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 })),
3160            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 })),
3161            // §Fase 111.f — `compute <Name> on a, b -> out`. The ARGUMENTS used to
3162            // be `Vec::new()` — hardcoded empty at the parse site — so even if
3163            // the runtime had wanted to compute something, it had nothing to
3164            // compute it FROM.
3165            TokenType::Compute => self.parse_compute_apply().map(FlowStep::ComputeApply),
3166            TokenType::Listen => self.parse_listen_step(),
3167            TokenType::Daemon => self.parse_flow_step_simple("daemon").map(|l| FlowStep::DaemonStep(DaemonStepNode { daemon_ref: l.1, loc: l.0 })),
3168            // §λ-L-E Fase 13 — Mobile typed channels (paper §3.1, §3.2, §4.3)
3169            TokenType::Emit => self.parse_emit_step(),
3170            // §Fase 92.b — `mint <Credential> as <binding>` (ephemeral credential).
3171            TokenType::Mint => self.parse_mint_step(),
3172            // §Fase 94.b — `rotate <SecretsStore> [where "…"] with <Tool> as
3173            // <binding>` (mediated secret renewal).
3174            TokenType::Rotate => self.parse_rotate_step(),
3175            TokenType::Publish => self.parse_publish_step(),
3176            TokenType::Discover => self.parse_discover_step(),
3177            TokenType::Persist => self.parse_persist_step(),
3178            TokenType::Retrieve => self.parse_retrieve_step(),
3179            TokenType::Mutate => self.parse_mutate_step(),
3180            TokenType::Purge => self.parse_store_where_step().map(|(loc, store_name, where_expr)| FlowStep::Purge(PurgeStep { store_name, where_expr, loc })),
3181            TokenType::Transact => self.parse_block_step("transact").map(|l| FlowStep::Transact(TransactBlock { loc: l })),
3182            // §Fase 88.a — the `warden` adversarial-analysis block.
3183            TokenType::Warden => self.parse_warden().map(FlowStep::Warden),
3184            // §Fase 51.a — the `quant` cognitive block (Hilbert-space projection).
3185            TokenType::Quant => self.parse_quant().map(FlowStep::Quant),
3186            // §Fase 51.d.2 — the `yield` measurement point.
3187            TokenType::Yield => self.parse_yield().map(FlowStep::Yield),
3188            // §Fase 52.c — `run <Flow>(args)` as a flow-step: invoke a declared
3189            // flow from inside a body (a `daemon` listen handler, Q3). Reuses
3190            // the top-level run parser.
3191            TokenType::Run => self.parse_run().map(FlowStep::Run),
3192
3193            _ => {
3194                // §Fase 28.e — append "Did you mean X?" hint when the
3195                // unknown token looks like a typo'd flow-body keyword
3196                // (e.g. `stepp` / `reasn` / `validte`). D3, D11.
3197                let hint = crate::smart_suggest::suggest_for(
3198                    &tok.value,
3199                    crate::smart_suggest::FLOW_BODY_KEYWORD_NAMES,
3200                );
3201                let base = format!(
3202                    "Unexpected token in flow body: '{}' — expected step, if, for, let, return, ...",
3203                    tok.value
3204                );
3205                let message = if hint.is_empty() {
3206                    base
3207                } else {
3208                    format!("{base}. {hint}")
3209                };
3210                Err(ParseError {
3211                    message,
3212                    line: tok.line,
3213                    column: tok.column,
3214                    ..Default::default()
3215                })
3216            }
3217        }
3218    }
3219
3220    // ── STEP ─────────────────────────────────────────────────────
3221
3222    fn parse_step(&mut self) -> Result<StepNode, ParseError> {
3223        let tok = self.consume(TokenType::Step)?;
3224        let loc = self.loc_of(&tok);
3225        let name = self.consume(TokenType::Identifier)?.value;
3226
3227        let mut persona_ref = String::new();
3228        if self.check(TokenType::Use) {
3229            self.advance();
3230            persona_ref = self.consume_any_ident_or_kw()?.value;
3231        }
3232
3233        self.consume(TokenType::LBrace)?;
3234
3235        let mut node = StepNode {
3236            name,
3237            persona_ref,
3238            given: String::new(),
3239            ask: String::new(),
3240            output_type: String::new(),
3241            confidence_floor: None,
3242            navigate_ref: String::new(),
3243            apply_ref: String::new(),
3244            requires_context: None,
3245            now_tz: None,
3246            loc,
3247        };
3248
3249        while !self.check(TokenType::RBrace) {
3250            let inner = self.current().clone();
3251
3252            match inner.ttype {
3253                TokenType::Given => {
3254                    self.advance();
3255                    self.consume(TokenType::Colon)?;
3256                    node.given = self.parse_expression_string()?;
3257                }
3258                TokenType::Ask => {
3259                    self.advance();
3260                    self.consume(TokenType::Colon)?;
3261                    node.ask = self.consume(TokenType::StringLit)?.value;
3262                }
3263                TokenType::Output => {
3264                    // Mirror of Python `_parse_step` `case "output":`
3265                    // which uses `_parse_output_type_string` — accepts
3266                    // the FULL generic-aware shape `Stream<T>`,
3267                    // `Stream<T>?`, `Identifier?`, NOT just the bare
3268                    // head identifier. Pre-fix the step parser dropped
3269                    // `<T>` and downstream `flow_has_stream_output`'s
3270                    // `starts_with("Stream<") && ends_with('>')` then
3271                    // returned false → `implicit_transport == "json"`
3272                    // → dynamic routes served JSON instead of SSE.
3273                    self.advance();
3274                    self.consume(TokenType::Colon)?;
3275                    node.output_type = self.parse_output_type_string()?;
3276                }
3277                TokenType::Navigate => {
3278                    self.advance();
3279                    self.consume(TokenType::Colon)?;
3280                    node.navigate_ref = self.parse_dotted_identifier()?;
3281                }
3282                TokenType::Identifier if inner.value == "confidence_floor" => {
3283                    self.advance();
3284                    self.consume(TokenType::Colon)?;
3285                    node.confidence_floor = Some(self.consume_number()?);
3286                }
3287                TokenType::Identifier if inner.value == "apply" => {
3288                    self.advance();
3289                    self.consume(TokenType::Colon)?;
3290                    node.apply_ref = self.consume_any_ident_or_kw()?.value;
3291                }
3292                // §Fase 68.b — `requires_context: <tokens>`: the step's declared
3293                // model-capability requirement (the context window the cognition
3294                // needs). A bare positive integer literal; the §68.c resolver maps
3295                // it to a concrete model. Range/ceiling is the type-checker's job
3296                // (§68.b positive-int + §68.f catalog ceiling) — the parser only
3297                // requires an integer token here (a float / non-number is a parse
3298                // error, surfaced at the exact column).
3299                TokenType::Identifier if inner.value == "requires_context" => {
3300                    self.advance();
3301                    self.consume(TokenType::Colon)?;
3302                    let num = self.current().clone();
3303                    let bad = |tok: &crate::tokens::Token| ParseError {
3304                        message: format!(
3305                            "`requires_context:` must be a positive integer token count \
3306                             (got '{}')",
3307                            tok.value
3308                        ),
3309                        line: tok.line,
3310                        column: tok.column,
3311                        ..Default::default()
3312                    };
3313                    if num.ttype != TokenType::Integer {
3314                        return Err(bad(&num));
3315                    }
3316                    let value = num.value.parse::<u32>().map_err(|_| bad(&num))?;
3317                    self.advance();
3318                    node.requires_context = Some(value);
3319                }
3320                // §Fase 91.a — `now: "<IANA-tz>"`: the step's declared cognitive
3321                // timezone. A string literal; the format law (IANA shape) is the
3322                // type-checker's job (`axon-T892`) — the parser only requires a
3323                // string token here, surfaced at the exact column.
3324                TokenType::Identifier if inner.value == "now" => {
3325                    self.advance();
3326                    self.consume(TokenType::Colon)?;
3327                    let tz = self.current().clone();
3328                    if tz.ttype != TokenType::StringLit {
3329                        return Err(ParseError {
3330                            message: format!(
3331                                "`now:` must be an IANA timezone string literal like \
3332                                 \"America/Bogota\" or \"UTC\" (got '{}')",
3333                                tz.value
3334                            ),
3335                            line: tz.line,
3336                            column: tz.column,
3337                            ..Default::default()
3338                        });
3339                    }
3340                    self.advance();
3341                    node.now_tz = Some(tz.value);
3342                }
3343                // §Fase 54.a — a `use` nested inside a `step { }` body used
3344                // to be skipped structurally (grouped with the sub-constructs
3345                // below), silently degrading the tool dispatch to an
3346                // unconstrained LLM step with NO diagnostic. That fallthrough
3347                // drops the AST node before the type-checker can see it, so the
3348                // resource the tool would provision is never linearly accounted
3349                // for (use_tool soundness). Reject it here, at the parser —
3350                // the only place that still sees the token — and redirect to
3351                // the canonical forms.
3352                TokenType::Use => {
3353                    let tool = self
3354                        .tokens
3355                        .get(self.pos + 1)
3356                        .map(|t| t.value.as_str())
3357                        .filter(|v| !v.is_empty())
3358                        .unwrap_or("<Tool>");
3359                    return Err(ParseError {
3360                        message: format!(
3361                            "`use` is not valid inside a `step {{ }}` body — the tool dispatch \
3362                             would be silently dropped. To invoke a tool, either write the \
3363                             flow-level step `use {tool} on <arg>` (outside this block), or bind \
3364                             it inside this step with `apply: {tool}`. To attach a persona, put \
3365                             it in the step header: `step <name> use <Persona> {{ … }}`."
3366                        ),
3367                        line: inner.line,
3368                        column: inner.column,
3369                        ..Default::default()
3370                    });
3371                }
3372                // Sub-constructs (probe, reason, weave, stream) → skip structurally
3373                TokenType::Probe
3374                | TokenType::Reason
3375                | TokenType::Weave
3376                | TokenType::Stream => {
3377                    self.skip_flow_step_structural()?;
3378                }
3379                _ => {
3380                    return Err(ParseError {
3381                        message: format!(
3382                            "Unexpected token in step body: '{}' — expected given, ask, \
3383                             probe, reason, weave, stream, output, confidence_floor, navigate, \
3384                             apply, requires_context, now",
3385                            inner.value
3386                        ),
3387                        line: inner.line,
3388                        column: inner.column,
3389                                            ..Default::default()
3390                    });
3391                }
3392            }
3393        }
3394        self.consume(TokenType::RBrace)?;
3395        Ok(node)
3396    }
3397
3398    /// Skip a flow-level sub-construct structurally (consume keyword + args + optional block).
3399    fn skip_flow_step_structural(&mut self) -> Result<(), ParseError> {
3400        // Consume the keyword
3401        self.advance();
3402        // Consume tokens until we hit a { or a closing }, or a known flow step keyword
3403        while !self.check(TokenType::LBrace)
3404            && !self.check(TokenType::RBrace)
3405            && !self.check(TokenType::Eof)
3406        {
3407            // Check if we hit a new step-level keyword (means this was a one-liner)
3408            let tt = &self.current().ttype;
3409            if matches!(
3410                tt,
3411                TokenType::Step
3412                    | TokenType::Given
3413                    | TokenType::Ask
3414                    | TokenType::Output
3415                    | TokenType::Navigate
3416                    | TokenType::Use
3417                    | TokenType::Probe
3418                    | TokenType::Reason
3419                    | TokenType::Weave
3420                    | TokenType::Stream
3421                    | TokenType::If
3422                    | TokenType::For
3423                    | TokenType::Let
3424                    | TokenType::Return
3425            ) {
3426                return Ok(());
3427            }
3428            self.advance();
3429        }
3430        // If block, skip it
3431        if self.check(TokenType::LBrace) {
3432            self.skip_braced_block()?;
3433        }
3434        Ok(())
3435    }
3436
3437    // ── INTENT ───────────────────────────────────────────────────
3438
3439    fn parse_intent(&mut self) -> Result<IntentNode, ParseError> {
3440        let tok = self.consume(TokenType::Intent)?;
3441        let loc = self.loc_of(&tok);
3442        let name = self.consume(TokenType::Identifier)?.value;
3443        self.consume(TokenType::LBrace)?;
3444
3445        let mut node = IntentNode {
3446            name,
3447            given: String::new(),
3448            ask: String::new(),
3449            output_type: None,
3450            confidence_floor: None,
3451            loc,
3452            leading_trivia: Vec::new(),
3453            trailing_trivia: Vec::new(),
3454        };
3455
3456        while !self.check(TokenType::RBrace) {
3457            let field_name = self.current().value.clone();
3458            self.advance();
3459            self.consume(TokenType::Colon)?;
3460
3461            match field_name.as_str() {
3462                "given" => node.given = self.consume(TokenType::Identifier)?.value,
3463                "ask" => node.ask = self.consume(TokenType::StringLit)?.value,
3464                "output" => node.output_type = Some(self.parse_type_expr()?),
3465                "confidence_floor" => node.confidence_floor = Some(self.consume_number()?),
3466                _ => self.skip_value(),
3467            }
3468        }
3469        self.consume(TokenType::RBrace)?;
3470        Ok(node)
3471    }
3472
3473    // ── RUN ──────────────────────────────────────────────────────
3474
3475    fn parse_run(&mut self) -> Result<RunStatement, ParseError> {
3476        let tok = self.consume(TokenType::Run)?;
3477        let loc = self.loc_of(&tok);
3478        let flow_name = self.consume(TokenType::Identifier)?.value;
3479
3480        self.consume(TokenType::LParen)?;
3481        let mut arguments = Vec::new();
3482        if !self.check(TokenType::RParen) {
3483            arguments = self.parse_argument_list()?;
3484        }
3485        self.consume(TokenType::RParen)?;
3486
3487        let mut node = RunStatement {
3488            flow_name,
3489            arguments,
3490            persona: String::new(),
3491            context: String::new(),
3492            anchors: Vec::new(),
3493            on_failure: String::new(),
3494            on_failure_params: Vec::new(),
3495            output_to: String::new(),
3496            effort: String::new(),
3497            loc,
3498            leading_trivia: Vec::new(),
3499            trailing_trivia: Vec::new(),
3500        };
3501
3502        while self.check_run_modifier() {
3503            let mod_tok = self.current().clone();
3504            match mod_tok.ttype {
3505                TokenType::As => {
3506                    self.advance();
3507                    node.persona = self.consume(TokenType::Identifier)?.value;
3508                }
3509                TokenType::Within => {
3510                    self.advance();
3511                    node.context = self.consume(TokenType::Identifier)?.value;
3512                }
3513                TokenType::ConstrainedBy => {
3514                    self.advance();
3515                    node.anchors = self.parse_bracketed_identifiers()?;
3516                }
3517                TokenType::OnFailure => {
3518                    self.advance();
3519                    self.consume(TokenType::Colon)?;
3520                    node.on_failure = self.consume_any_ident_or_kw()?.value;
3521                    // Parse optional params: (key: val, ...)
3522                    if self.check(TokenType::LParen) {
3523                        self.advance();
3524                        while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
3525                            let key = self.consume_any_ident_or_kw()?.value;
3526                            self.consume(TokenType::Colon)?;
3527                            let val = self.consume_any_ident_or_kw()?.value;
3528                            node.on_failure_params.push((key, val));
3529                            if self.check(TokenType::Comma) {
3530                                self.advance();
3531                            }
3532                        }
3533                        if self.check(TokenType::RParen) {
3534                            self.advance();
3535                        }
3536                    }
3537                }
3538                TokenType::OutputTo => {
3539                    self.advance();
3540                    self.consume(TokenType::Colon)?;
3541                    node.output_to = self.consume(TokenType::StringLit)?.value;
3542                }
3543                TokenType::Effort => {
3544                    self.advance();
3545                    self.consume(TokenType::Colon)?;
3546                    node.effort = self.consume_any_ident_or_kw()?.value;
3547                }
3548                _ => break,
3549            }
3550        }
3551
3552        Ok(node)
3553    }
3554
3555    // ── EPISTEMIC BLOCK ──────────────────────────────────────────
3556
3557    fn parse_epistemic_block(&mut self) -> Result<EpistemicBlock, ParseError> {
3558        let tok = self.current().clone();
3559        let mode = match tok.ttype {
3560            TokenType::Know => "know",
3561            TokenType::Believe => "believe",
3562            TokenType::Speculate => "speculate",
3563            TokenType::Doubt => "doubt",
3564            _ => unreachable!(),
3565        };
3566        self.advance();
3567        let loc = self.loc_of(&tok);
3568
3569        self.consume(TokenType::LBrace)?;
3570        let mut body = Vec::new();
3571        while !self.check(TokenType::RBrace) {
3572            body.push(self.parse_declaration()?);
3573        }
3574        self.consume(TokenType::RBrace)?;
3575
3576        Ok(EpistemicBlock {
3577            mode: mode.to_string(),
3578            body,
3579            loc,
3580            leading_trivia: Vec::new(),
3581            trailing_trivia: Vec::new(),
3582        })
3583    }
3584
3585    // ── IF ────────────────────────────────────────────────────────
3586
3587    // ── §Fase 70.a — the pure expression engine (Pratt parser) ───────────
3588
3589    /// Parse a pure expression (§Fase 70). Precedence-climbing: `or` < `and` <
3590    /// comparison < `+ -` < `* / %` < unary (`- not`) < atom. Total + pure; no
3591    /// side effects. Field/index access + the builtin catalog land in §70.c/d.
3592    fn parse_expr(&mut self) -> Result<Expr, ParseError> {
3593        self.parse_expr_bp(0)
3594    }
3595
3596    fn parse_expr_bp(&mut self, min_bp: u8) -> Result<Expr, ParseError> {
3597        // Prefix: unary `-` (negation) / `not` (boolean). Binds tighter than
3598        // every binary operator (bp 6).
3599        let mut lhs = match self.current().ttype {
3600            TokenType::Minus => {
3601                self.advance();
3602                Expr::Unary(UnOp::Neg, Box::new(self.parse_expr_bp(6)?))
3603            }
3604            TokenType::Not => {
3605                self.advance();
3606                Expr::Unary(UnOp::Not, Box::new(self.parse_expr_bp(6)?))
3607            }
3608            _ => self.parse_postfix()?,
3609        };
3610        // Infix: left-associative (right_bp = left_bp + 1).
3611        while let Some((op, lbp)) = Self::binop_of(self.current().ttype.clone()) {
3612            if lbp < min_bp {
3613                break;
3614            }
3615            self.advance();
3616            let rhs = self.parse_expr_bp(lbp + 1)?;
3617            lhs = Expr::Binary(op, Box::new(lhs), Box::new(rhs));
3618        }
3619        Ok(lhs)
3620    }
3621
3622    /// Map a token to `(BinOp, left binding power)`, or `None` if it is not an
3623    /// infix operator (which stops the climb — e.g. at `->` or `{`).
3624    fn binop_of(t: TokenType) -> Option<(BinOp, u8)> {
3625        Some(match t {
3626            TokenType::Or => (BinOp::Or, 1),
3627            TokenType::And => (BinOp::And, 2),
3628            TokenType::Eq => (BinOp::Eq, 3),
3629            TokenType::Neq => (BinOp::Ne, 3),
3630            TokenType::Lt => (BinOp::Lt, 3),
3631            TokenType::Lte => (BinOp::Le, 3),
3632            TokenType::Gt => (BinOp::Gt, 3),
3633            TokenType::Gte => (BinOp::Ge, 3),
3634            TokenType::Plus => (BinOp::Add, 4),
3635            TokenType::Minus => (BinOp::Sub, 4),
3636            TokenType::Star => (BinOp::Mul, 5),
3637            TokenType::Slash => (BinOp::Div, 5),
3638            TokenType::Percent => (BinOp::Mod, 5),
3639            _ => return None,
3640        })
3641    }
3642
3643    /// §Fase 70.c — parse a primary then its `.` postfix chain: a builtin call
3644    /// (`.length`, `.contains(x)`) when the name is in the closed catalog, else
3645    /// a dotted reference-path continuation (`a.b.c` → `Ref("a.b.c")`, the
3646    /// pre-§70.c behaviour). Field access on a non-reference (`(a+b).x`) is
3647    /// reserved for §70.d.
3648    fn parse_postfix(&mut self) -> Result<Expr, ParseError> {
3649        let mut expr = self.parse_expr_atom()?;
3650        loop {
3651            if self.check(TokenType::Dot) {
3652                self.advance();
3653                let name = self.consume_any_ident_or_kw()?.value;
3654                if let Some(builtin) = Builtin::from_name(&name) {
3655                    let mut args = vec![expr];
3656                    if self.check(TokenType::LParen) {
3657                        self.advance();
3658                        while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
3659                            args.push(self.parse_expr_bp(0)?);
3660                            if self.check(TokenType::Comma) {
3661                                self.advance();
3662                            } else {
3663                                break;
3664                            }
3665                        }
3666                        self.consume(TokenType::RParen)?;
3667                    }
3668                    expr = Expr::Call(builtin, args);
3669                } else {
3670                    // §Fase 70.d — a plain dotted path on a Ref extends the Ref
3671                    // (back-compat: `a.b.c` → `Ref("a.b.c")`); on any other base
3672                    // it is a structured field access (the JSONB seam).
3673                    expr = match expr {
3674                        Expr::Ref(p) => Expr::Ref(format!("{p}.{name}")),
3675                        other => Expr::Field(Box::new(other), name),
3676                    };
3677                }
3678            } else if self.check(TokenType::LBracket) {
3679                // §Fase 70.d — index access `base[index]`.
3680                self.advance();
3681                let index = self.parse_expr_bp(0)?;
3682                self.consume(TokenType::RBracket)?;
3683                expr = Expr::Index(Box::new(expr), Box::new(index));
3684            } else {
3685                break;
3686            }
3687        }
3688        Ok(expr)
3689    }
3690
3691    fn parse_expr_atom(&mut self) -> Result<Expr, ParseError> {
3692        let tok = self.current().clone();
3693        match tok.ttype {
3694            TokenType::Integer => {
3695                self.advance();
3696                let lit = tok
3697                    .value
3698                    .parse::<i64>()
3699                    .map(ExprLit::Int)
3700                    .or_else(|_| tok.value.parse::<f64>().map(ExprLit::Float))
3701                    .map_err(|_| ParseError {
3702                        message: format!("invalid integer literal '{}'", tok.value),
3703                        line: tok.line,
3704                        column: tok.column,
3705                        ..Default::default()
3706                    })?;
3707                Ok(Expr::Lit(lit))
3708            }
3709            TokenType::Float => {
3710                self.advance();
3711                let f = tok.value.parse::<f64>().map_err(|_| ParseError {
3712                    message: format!("invalid float literal '{}'", tok.value),
3713                    line: tok.line,
3714                    column: tok.column,
3715                    ..Default::default()
3716                })?;
3717                Ok(Expr::Lit(ExprLit::Float(f)))
3718            }
3719            TokenType::Bool => {
3720                self.advance();
3721                Ok(Expr::Lit(ExprLit::Bool(tok.value == "true")))
3722            }
3723            TokenType::StringLit => {
3724                self.advance();
3725                Ok(Expr::Lit(ExprLit::Str(tok.value)))
3726            }
3727            TokenType::LParen => {
3728                self.advance();
3729                let inner = self.parse_expr_bp(0)?;
3730                self.consume(TokenType::RParen)?;
3731                Ok(inner)
3732            }
3733            _ => {
3734                // Reference: a single identifier (or keyword used as a name).
3735                // The `.` chain (dotted path / builtin call) is handled by the
3736                // postfix layer (§70.c `parse_postfix`).
3737                Ok(Expr::Ref(self.consume_any_ident_or_kw()?.value))
3738            }
3739        }
3740    }
3741
3742    /// §Fase 70.a — render a literal to its legacy surface string (for the
3743    /// back-compat `(condition, op, value)` triple). Only used when an
3744    /// expression fits the legacy shape; numeric round-tripping is exact for
3745    /// ints and faithful-enough for floats (the legacy runtime re-parses it).
3746    fn expr_lit_surface(lit: &ExprLit) -> String {
3747        match lit {
3748            ExprLit::Int(i) => i.to_string(),
3749            ExprLit::Float(f) => f.to_string(),
3750            ExprLit::Bool(b) => b.to_string(),
3751            ExprLit::Str(s) => s.clone(),
3752        }
3753    }
3754
3755    fn expr_leaf_surface(expr: &Expr) -> Option<String> {
3756        match expr {
3757            Expr::Ref(p) => Some(p.clone()),
3758            Expr::Lit(l) => Some(Self::expr_lit_surface(l)),
3759            _ => None,
3760        }
3761    }
3762
3763    /// A legacy "leaf" is a bare reference (truthy check) or a
3764    /// `<ref> <cmp> <ref|literal>` triple — exactly what the pre-§70 `if`
3765    /// grammar could express.
3766    fn expr_legacy_leaf(expr: &Expr) -> Option<(String, String, String)> {
3767        match expr {
3768            Expr::Ref(p) => Some((p.clone(), String::new(), String::new())),
3769            Expr::Binary(op, l, r) => {
3770                let op_s = match op {
3771                    BinOp::Eq => "==",
3772                    BinOp::Ne => "!=",
3773                    BinOp::Lt => "<",
3774                    BinOp::Le => "<=",
3775                    BinOp::Gt => ">",
3776                    BinOp::Ge => ">=",
3777                    _ => return None,
3778                };
3779                let lhs = match &**l {
3780                    Expr::Ref(p) => p.clone(),
3781                    _ => return None,
3782                };
3783                let rhs = Self::expr_leaf_surface(r)?;
3784                Some((lhs, op_s.to_string(), rhs))
3785            }
3786            _ => None,
3787        }
3788    }
3789
3790    /// Flatten an `or`-tree of legacy leaves in left-to-right order. Returns
3791    /// `false` (and leaves `out` unusable) if any node is not a legacy leaf.
3792    fn collect_or_leaves(expr: &Expr, out: &mut Vec<(String, String, String)>) -> bool {
3793        match expr {
3794            Expr::Binary(BinOp::Or, l, r) => {
3795                Self::collect_or_leaves(l, out) && Self::collect_or_leaves(r, out)
3796            }
3797            _ => match Self::expr_legacy_leaf(expr) {
3798                Some(t) => {
3799                    out.push(t);
3800                    true
3801                }
3802                None => false,
3803            },
3804        }
3805    }
3806
3807    /// §Fase 70.a — if the parsed condition fits the legacy
3808    /// `(condition, op, value)` + `or`-chain shape, return the legacy fields so
3809    /// the IR + runtime stay byte-identical to pre-§70 (zero drift). `None` ⇒
3810    /// the condition uses richer forms (`and`, `not`, arithmetic, parentheses,
3811    /// nesting) and must ride the `cond` expression evaluator.
3812    #[allow(clippy::type_complexity)]
3813    fn cond_as_legacy(
3814        expr: &Expr,
3815    ) -> Option<(String, String, String, Vec<(String, String, String)>, String)> {
3816        let mut leaves = Vec::new();
3817        if !Self::collect_or_leaves(expr, &mut leaves) || leaves.is_empty() {
3818            return None;
3819        }
3820        let (c0, o0, v0) = leaves[0].clone();
3821        let rest = leaves[1..].to_vec();
3822        let conjunctor = if rest.is_empty() {
3823            String::new()
3824        } else {
3825            "or".to_string()
3826        };
3827        Some((c0, o0, v0, rest, conjunctor))
3828    }
3829
3830    fn parse_if(&mut self) -> Result<ConditionalNode, ParseError> {
3831        let tok = self.consume(TokenType::If)?;
3832        let loc = self.loc_of(&tok);
3833
3834        // §Fase 70.a — parse the condition as a pure expression, then split:
3835        // a legacy-expressible condition populates the legacy triple fields
3836        // (cond = None → byte-identical IR + eval); a richer condition rides
3837        // the `cond` expression evaluator.
3838        let expr = self.parse_expr()?;
3839        let (condition, comparison_op, comparison_value, conditions, conjunctor, cond) =
3840            match Self::cond_as_legacy(&expr) {
3841                Some((c, o, v, more, conj)) => (c, o, v, more, conj, None),
3842                None => (
3843                    String::new(),
3844                    String::new(),
3845                    String::new(),
3846                    Vec::new(),
3847                    String::new(),
3848                    Some(expr),
3849                ),
3850            };
3851
3852        let mut then_body = Vec::new();
3853        let mut else_body = Vec::new();
3854
3855        // Arrow form or block form
3856        if self.check(TokenType::Arrow) {
3857            self.advance();
3858            then_body.push(self.parse_flow_step()?);
3859        } else if self.check(TokenType::LBrace) {
3860            self.advance();
3861            while !self.check(TokenType::RBrace) {
3862                then_body.push(self.parse_flow_step()?);
3863            }
3864            self.consume(TokenType::RBrace)?;
3865        }
3866
3867        // Else branch
3868        if self.check(TokenType::Else) {
3869            self.advance();
3870            if self.check(TokenType::Arrow) {
3871                self.advance();
3872                else_body.push(self.parse_flow_step()?);
3873            } else if self.check(TokenType::LBrace) {
3874                self.advance();
3875                while !self.check(TokenType::RBrace) {
3876                    else_body.push(self.parse_flow_step()?);
3877                }
3878                self.consume(TokenType::RBrace)?;
3879            }
3880        }
3881
3882        Ok(ConditionalNode {
3883            condition,
3884            comparison_op,
3885            comparison_value,
3886            then_body,
3887            else_body,
3888            conditions,
3889            conjunctor,
3890            cond,
3891            loc,
3892        })
3893    }
3894
3895    // ── FOR IN ───────────────────────────────────────────────────
3896
3897    fn parse_for_in(&mut self) -> Result<ForInStatement, ParseError> {
3898        let tok = self.consume(TokenType::For)?;
3899        let loc = self.loc_of(&tok);
3900        let variable = self.consume(TokenType::Identifier)?.value;
3901        self.consume(TokenType::In)?;
3902        let iterable = self.parse_dotted_identifier()?;
3903
3904        self.consume(TokenType::LBrace)?;
3905        // Fase 19.e — increment loop_depth so `parse_break` /
3906        // `parse_continue` inside the body pass the scope check.
3907        // Decrement on every exit path (Ok / Err) so a parse error
3908        // mid-body does not leave the depth permanently elevated
3909        // for later top-level parsing — `?` would skip the
3910        // decrement otherwise.
3911        self.loop_depth += 1;
3912        let body_result = (|| -> Result<Vec<FlowStep>, ParseError> {
3913            let mut body = Vec::new();
3914            while !self.check(TokenType::RBrace) {
3915                body.push(self.parse_flow_step()?);
3916            }
3917            Ok(body)
3918        })();
3919        self.loop_depth -= 1;
3920        let body = body_result?;
3921        self.consume(TokenType::RBrace)?;
3922
3923        Ok(ForInStatement {
3924            variable,
3925            iterable,
3926            body,
3927            loc,
3928        })
3929    }
3930
3931    /// Fase 19.e — `break` keyword. Compile-time scope check
3932    /// (`loop_depth == 0`) rejects break outside a for-in body.
3933    fn parse_break(&mut self) -> Result<BreakStatement, ParseError> {
3934        let tok = self.consume(TokenType::Break)?;
3935        let loc = self.loc_of(&tok);
3936        if self.loop_depth == 0 {
3937            return Err(ParseError {
3938                message: "'break' outside of a for-in loop body".to_string(),
3939                line: tok.line,
3940                column: tok.column,
3941                            ..Default::default()
3942            });
3943        }
3944        Ok(BreakStatement { loc })
3945    }
3946
3947    /// Fase 19.e — `continue` keyword. Same scope check as
3948    /// `parse_break`.
3949    fn parse_continue(&mut self) -> Result<ContinueStatement, ParseError> {
3950        let tok = self.consume(TokenType::Continue)?;
3951        let loc = self.loc_of(&tok);
3952        if self.loop_depth == 0 {
3953            return Err(ParseError {
3954                message: "'continue' outside of a for-in loop body".to_string(),
3955                line: tok.line,
3956                column: tok.column,
3957                            ..Default::default()
3958            });
3959        }
3960        Ok(ContinueStatement { loc })
3961    }
3962
3963    // ── LET ──────────────────────────────────────────────────────
3964
3965    fn parse_let(&mut self) -> Result<LetStatement, ParseError> {
3966        let tok = self.consume(TokenType::Let)?;
3967        let loc = self.loc_of(&tok);
3968
3969        // Name can be an identifier or a keyword used as binding name
3970        let name = self.consume_any_ident_or_kw()?.value;
3971        // §Fase 51.c.3 — optional type annotation `let x: <TypeExpr> = …`.
3972        let type_annotation = if self.check(TokenType::Colon) {
3973            self.advance();
3974            Some(self.parse_type_expr()?)
3975        } else {
3976            None
3977        };
3978        self.consume(TokenType::Assign)?;
3979        // Fase 17.a — reset side-channel before parsing value; the
3980        // atom / expr helpers tag the kind as they descend.
3981        self.last_let_value_kind = "literal".to_string();
3982        let (value, value_ast) = self.parse_let_value_expr_with_ast()?;
3983
3984        Ok(LetStatement {
3985            identifier: name,
3986            value_expr: value,
3987            value_kind: self.last_let_value_kind.clone(),
3988            type_annotation,
3989            value_ast,
3990            loc,
3991            leading_trivia: Vec::new(),
3992            trailing_trivia: Vec::new(),
3993        })
3994    }
3995
3996    fn parse_let_value_expr(&mut self) -> Result<String, ParseError> {
3997        let atom = self.parse_let_atom()?;
3998
3999        // Arithmetic expression: collect as string
4000        if matches!(
4001            self.current().ttype,
4002            TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
4003        ) {
4004            let mut parts = vec![atom];
4005            while matches!(
4006                self.current().ttype,
4007                TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
4008            ) {
4009                parts.push(self.advance().value.clone());
4010                parts.push(self.parse_let_atom()?);
4011            }
4012            self.last_let_value_kind = "expression".to_string();
4013            return Ok(parts.join(" "));
4014        }
4015        Ok(atom)
4016    }
4017
4018    /// §Fase 70.f — parse a `let`-binding value, additionally producing a
4019    /// structured `value_ast` for the expression case. A list literal keeps the
4020    /// dedicated path; everything else is parsed through the §70 expression
4021    /// engine and classified: a bare literal / reference keeps its pre-§70
4022    /// string form (`value_ast = None`, byte-identical), while a real expression
4023    /// (`price * qty`, `recent.length`) additionally carries a `value_ast` the
4024    /// runtime evaluates for real (pre-§70.f it was treated as an opaque literal
4025    /// string). Used ONLY by `parse_let` — other value positions (list items,
4026    /// remember/stream values) keep the string-only `parse_let_value_expr`.
4027    fn parse_let_value_expr_with_ast(&mut self) -> Result<(String, Option<Expr>), ParseError> {
4028        if self.check(TokenType::LBracket) {
4029            self.last_let_value_kind = "literal".to_string();
4030            return Ok((self.parse_let_list_literal()?, None));
4031        }
4032        let expr = self.parse_expr()?;
4033        Ok(match expr {
4034            Expr::Lit(lit) => {
4035                self.last_let_value_kind = "literal".to_string();
4036                (Self::expr_lit_surface(&lit), None)
4037            }
4038            Expr::Ref(p) => {
4039                self.last_let_value_kind = "reference".to_string();
4040                (p, None)
4041            }
4042            other => {
4043                self.last_let_value_kind = "expression".to_string();
4044                (Self::render_expr(&other), Some(other))
4045            }
4046        })
4047    }
4048
4049    /// §Fase 70.f — a readable surface rendering of an expression for the
4050    /// vestigial `value_expr` string (the runtime uses `value_ast`).
4051    fn render_expr(e: &Expr) -> String {
4052        match e {
4053            Expr::Lit(l) => Self::expr_lit_surface(l),
4054            Expr::Ref(p) => p.clone(),
4055            Expr::Unary(UnOp::Neg, x) => format!("-{}", Self::render_expr(x)),
4056            Expr::Unary(UnOp::Not, x) => format!("not {}", Self::render_expr(x)),
4057            Expr::Binary(op, l, r) => {
4058                let sym = match op {
4059                    BinOp::Add => "+",
4060                    BinOp::Sub => "-",
4061                    BinOp::Mul => "*",
4062                    BinOp::Div => "/",
4063                    BinOp::Mod => "%",
4064                    BinOp::Eq => "==",
4065                    BinOp::Ne => "!=",
4066                    BinOp::Lt => "<",
4067                    BinOp::Le => "<=",
4068                    BinOp::Gt => ">",
4069                    BinOp::Ge => ">=",
4070                    BinOp::And => "and",
4071                    BinOp::Or => "or",
4072                };
4073                format!("({} {sym} {})", Self::render_expr(l), Self::render_expr(r))
4074            }
4075            Expr::Call(b, args) => {
4076                let recv = args.first().map(Self::render_expr).unwrap_or_default();
4077                let rest: Vec<String> = args.iter().skip(1).map(Self::render_expr).collect();
4078                if rest.is_empty() {
4079                    format!("{recv}.{}", b.surface())
4080                } else {
4081                    format!("{recv}.{}({})", b.surface(), rest.join(", "))
4082                }
4083            }
4084            Expr::Field(b, f) => format!("{}.{f}", Self::render_expr(b)),
4085            Expr::Index(b, i) => format!("{}[{}]", Self::render_expr(b), Self::render_expr(i)),
4086        }
4087    }
4088
4089    fn parse_let_atom(&mut self) -> Result<String, ParseError> {
4090        let tok = self.current().clone();
4091
4092        match tok.ttype {
4093            TokenType::StringLit => {
4094                self.last_let_value_kind = "literal".to_string();
4095                self.advance();
4096                Ok(tok.value)
4097            }
4098            TokenType::Integer | TokenType::Float => {
4099                self.last_let_value_kind = "literal".to_string();
4100                self.advance();
4101                Ok(tok.value)
4102            }
4103            TokenType::Bool => {
4104                self.last_let_value_kind = "literal".to_string();
4105                self.advance();
4106                Ok(tok.value)
4107            }
4108            TokenType::Identifier => {
4109                self.last_let_value_kind = "reference".to_string();
4110                self.parse_dotted_identifier()
4111            }
4112            TokenType::LBracket => {
4113                self.last_let_value_kind = "literal".to_string();
4114                self.parse_let_list_literal()
4115            }
4116            _ => {
4117                // Keywords starting a dotted path (pix.document_tree)
4118                if self.pos + 1 < self.tokens.len()
4119                    && self.tokens[self.pos + 1].ttype == TokenType::Dot
4120                {
4121                    self.last_let_value_kind = "reference".to_string();
4122                    return self.parse_dotted_identifier();
4123                }
4124                Err(ParseError {
4125                    message: format!(
4126                        "Expected value expression, found {:?}('{}')",
4127                        tok.ttype, tok.value
4128                    ),
4129                    line: tok.line,
4130                    column: tok.column,
4131                                    ..Default::default()
4132                })
4133            }
4134        }
4135    }
4136
4137    fn parse_let_list_literal(&mut self) -> Result<String, ParseError> {
4138        self.consume(TokenType::LBracket)?;
4139        let mut items = Vec::new();
4140        if !self.check(TokenType::RBracket) {
4141            items.push(self.parse_let_value_expr()?);
4142            while self.check(TokenType::Comma) {
4143                self.advance();
4144                if self.check(TokenType::RBracket) {
4145                    break; // trailing comma
4146                }
4147                items.push(self.parse_let_value_expr()?);
4148            }
4149        }
4150        self.consume(TokenType::RBracket)?;
4151        Ok(format!("[{}]", items.join(", ")))
4152    }
4153
4154    // ── RETURN ───────────────────────────────────────────────────
4155
4156    fn parse_return(&mut self) -> Result<ReturnStatement, ParseError> {
4157        let tok = self.consume(TokenType::Return)?;
4158        let loc = self.loc_of(&tok);
4159        let value = self.parse_let_value_expr()?;
4160        Ok(ReturnStatement {
4161            value_expr: value,
4162            loc,
4163        })
4164    }
4165
4166    // ── TIER 2 FLOW STEP HELPERS ────────────────────────────────────
4167
4168    /// Parse: keyword target (consumes keyword + one identifier/keyword-as-value).
4169    fn parse_flow_step_simple(&mut self, _kw: &str) -> Result<(Loc, String), ParseError> {
4170        let tok = self.current().clone();
4171        self.advance(); // consume keyword
4172        let target = if self.at_declaration_start()
4173            || self.check(TokenType::RBrace)
4174            || self.check(TokenType::Eof)
4175        {
4176            String::new()
4177        } else {
4178            self.consume_any_ident_or_kw()?.value.clone()
4179        };
4180        // Skip optional braced block
4181        if self.check(TokenType::LBrace) {
4182            self.skip_braced_block()?;
4183        }
4184        Ok((
4185            Loc {
4186                line: tok.line,
4187                column: tok.column,
4188            },
4189            target,
4190        ))
4191    }
4192
4193    /// Parse: keyword { ... } — block-level step, skip body structurally.
4194    /// §Fase 111.e — `stream { <steps> }` with a REAL body.
4195    ///
4196    /// The four block primitives (`deliberate`, `consensus`, `stream`,
4197    /// `transact`) all went through [`Self::parse_block_step`], whose entire job
4198    /// is `skip_braced_block()`. Their bodies were discarded at parse time — so
4199    /// their handlers were not no-ops through neglect, they were no-ops
4200    /// *by construction*: there was nothing in the AST to execute. §111 retracted
4201    /// `transact`; this gives `stream` its body back. `deliberate` / `consensus`
4202    /// remain body-less pending their Tier-4 disposition.
4203    fn parse_stream_block(&mut self) -> Result<StreamBlock, ParseError> {
4204        let tok = self.current().clone();
4205        let loc = self.loc_of(&tok);
4206        self.advance(); // consume `stream`
4207
4208        // Tolerate the pre-111 form `stream <effect-ish tokens> { … }`: skip any
4209        // argument tokens ahead of the brace, exactly as `parse_block_step` did,
4210        // so an existing program keeps parsing. Only the BODY changes.
4211        while !self.check(TokenType::LBrace)
4212            && !self.check(TokenType::RBrace)
4213            && !self.check(TokenType::Eof)
4214            && !self.at_declaration_start()
4215        {
4216            self.advance();
4217        }
4218
4219        let mut body = Vec::new();
4220        if self.check(TokenType::LBrace) {
4221            self.advance();
4222            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4223                body.push(self.parse_flow_step()?);
4224            }
4225            self.consume(TokenType::RBrace)?;
4226        }
4227
4228        Ok(StreamBlock { body, loc })
4229    }
4230
4231    fn parse_block_step(&mut self, _kw: &str) -> Result<Loc, ParseError> {
4232        let tok = self.current().clone();
4233        self.advance();
4234        // Skip optional arguments before brace
4235        while !self.check(TokenType::LBrace)
4236            && !self.check(TokenType::RBrace)
4237            && !self.check(TokenType::Eof)
4238            && !self.at_declaration_start()
4239        {
4240            self.advance();
4241        }
4242        if self.check(TokenType::LBrace) {
4243            self.skip_braced_block()?;
4244        }
4245        Ok(Loc {
4246            line: tok.line,
4247            column: tok.column,
4248        })
4249    }
4250
4251    /// §Fase 86 — parse `forge <Name>(seed: "<text>") -> <Type> { mode:,
4252    /// novelty:, depth:, branches:, constraints: }`. Real field capture
4253    /// (replacing the pre-§86 discard-everything stub). Strict closed-catalog:
4254    /// an unknown field is a hard parse error; all cross-field laws (Boden mode
4255    /// catalog, novelty range, depth/branches ≥ 1, `constraints:` → `anchor`)
4256    /// are §86.c type-checker territory.
4257    fn parse_forge_step(&mut self) -> Result<ForgeBlock, ParseError> {
4258        let tok = self.consume(TokenType::Forge)?;
4259        let name = self.consume(TokenType::Identifier)?.value;
4260        let mut node = ForgeBlock {
4261            name,
4262            novelty: 0.5,
4263            depth: 1,
4264            branches: 1,
4265            loc: Loc { line: tok.line, column: tok.column },
4266            ..Default::default()
4267        };
4268        // `(seed: "...")`
4269        self.consume(TokenType::LParen)?;
4270        let arg = self.consume_any_ident_or_kw()?.value;
4271        self.consume(TokenType::Colon)?;
4272        if arg != "seed" {
4273            return Err(self.error(&format!(
4274                "forge '{}' expects `seed:` as its argument, found `{arg}`",
4275                node.name
4276            )));
4277        }
4278        node.seed = self.consume(TokenType::StringLit)?.value;
4279        self.consume(TokenType::RParen)?;
4280        // `-> <Type>`
4281        self.consume(TokenType::Arrow)?;
4282        node.output_type = self.consume_any_ident_or_kw()?.value;
4283        // `{ fields }`
4284        self.consume(TokenType::LBrace)?;
4285        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4286            let field = self.consume_any_ident_or_kw()?.value;
4287            self.consume(TokenType::Colon)?;
4288            match field.as_str() {
4289                "mode" => node.mode = self.consume_any_ident_or_kw()?.value,
4290                "novelty" => node.novelty = self.consume_number()?,
4291                "depth" => {
4292                    node.depth = self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0)
4293                }
4294                "branches" => {
4295                    node.branches =
4296                        self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0)
4297                }
4298                "constraints" => node.constraints_ref = self.consume_any_ident_or_kw()?.value,
4299                other => {
4300                    return Err(self.error(&format!("unknown forge field `{other}`")))
4301                }
4302            }
4303            if self.check(TokenType::Comma) {
4304                self.consume(TokenType::Comma)?;
4305            }
4306        }
4307        self.consume(TokenType::RBrace)?;
4308        Ok(node)
4309    }
4310
4311    /// §Fase 65 — Parse `par { stmt1  stmt2  … }` into CONCURRENT branches.
4312    /// Each top-level flow statement inside the block is one branch (a
4313    /// single-statement body); they execute concurrently at runtime
4314    /// (`flow_dispatcher::parallel::run_branches_concurrently`). Before §65 the
4315    /// `par` body was skipped (`parse_block_step`), so the branches were lost
4316    /// and the handler ran as a stub. Multi-statement branches (grouping
4317    /// several steps into one sequential branch) are a future grammar
4318    /// extension; today the natural `par { step A  step B }` fans A and B out.
4319    fn parse_par_block(&mut self) -> Result<ParBlock, ParseError> {
4320        let tok = self.current().clone();
4321        self.advance(); // consume `par`
4322        self.consume(TokenType::LBrace)?;
4323        let mut branches: Vec<Vec<FlowStep>> = Vec::new();
4324        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4325            branches.push(vec![self.parse_flow_step()?]);
4326        }
4327        self.consume(TokenType::RBrace)?;
4328        Ok(ParBlock {
4329            branches,
4330            loc: Loc {
4331                line: tok.line,
4332                column: tok.column,
4333            },
4334        })
4335    }
4336
4337    /// §Fase 51.a — Parse the `quant` cognitive block surface.
4338    ///
4339    /// Grammar (the attribute header is OPTIONAL):
4340    /// ```text
4341    /// quant { <flow steps> }
4342    /// quant(encoding: amplitude, observable: M, qubits: 10,
4343    ///       depth: 4, bandwidth: 0.5, reupload: 3, backend: quant_sim) { <flow steps> }
4344    /// ```
4345    /// The bare form (the paper's example) leaves every attribute defaulted
4346    /// (`encoding = amplitude`, `effect = quant_sim`). The body is parsed into
4347    /// real nested `FlowStep`s — like `par` branches — so §51.b's Continuous
4348    /// Type Invariant scans actual AST rather than skipped tokens.
4349    /// §Fase 88.a — parse `warden(<target>) within <Scope> { <body> }`. The
4350    /// `within <Scope>` clause is MANDATORY at the grammar level (fail-closed by
4351    /// construction: a scopeless warden cannot be written); §88.c checks the
4352    /// scope RESOLVES + the target is in its allowlist.
4353    fn parse_warden(&mut self) -> Result<WardenBlock, ParseError> {
4354        let tok = self.consume(TokenType::Warden)?;
4355        // `(<target>)` — the resource under analysis.
4356        self.consume(TokenType::LParen)?;
4357        let target = self.consume_any_ident_or_kw()?.value;
4358        self.consume(TokenType::RParen)?;
4359        // `within <Scope>` — MANDATORY. Omitting it is a hard parse error.
4360        self.consume(TokenType::Within)?;
4361        let scope_ref = self.consume(TokenType::Identifier)?.value;
4362        let mut block = WardenBlock {
4363            target,
4364            scope_ref,
4365            body: Vec::new(),
4366            loc: Loc {
4367                line: tok.line,
4368                column: tok.column,
4369            },
4370        };
4371        // Body: real nested flow steps (like `quant`/`par`).
4372        self.consume(TokenType::LBrace)?;
4373        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4374            block.body.push(self.parse_flow_step()?);
4375        }
4376        self.consume(TokenType::RBrace)?;
4377        Ok(block)
4378    }
4379
4380    /// §Fase 88.a — parse `scope <Name> { targets: [ … ], depth: <ident>,
4381    /// approver: [requires] "<cap>" }`. Flat key:value block (the `cache` shape).
4382    /// Catalog + non-empty validation is §88.c. Unknown fields are a hard error
4383    /// (D83.7): a scope governs an offensive-capable analysis.
4384    fn parse_scope(&mut self) -> Result<ScopeDefinition, ParseError> {
4385        let tok = self.consume(TokenType::Scope)?;
4386        let name = self.consume(TokenType::Identifier)?.value;
4387        let mut node = ScopeDefinition {
4388            name,
4389            loc: Loc {
4390                line: tok.line,
4391                column: tok.column,
4392            },
4393            ..Default::default()
4394        };
4395        self.consume(TokenType::LBrace)?;
4396        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4397            let key = self.consume_any_ident_or_kw()?.value;
4398            self.consume(TokenType::Colon)?;
4399            match key.as_str() {
4400                "targets" => {
4401                    self.consume(TokenType::LBracket)?;
4402                    while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
4403                        let t = if self.check(TokenType::StringLit) {
4404                            self.consume(TokenType::StringLit)?.value
4405                        } else {
4406                            self.consume_any_ident_or_kw()?.value
4407                        };
4408                        node.targets.push(t);
4409                        if self.check(TokenType::Comma) {
4410                            self.advance();
4411                        }
4412                    }
4413                    self.consume(TokenType::RBracket)?;
4414                }
4415                "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
4416                "approver" => {
4417                    // Optional `requires` sugar before the capability string.
4418                    if self.current().value == "requires" {
4419                        self.advance();
4420                    }
4421                    node.approver = self.consume(TokenType::StringLit)?.value;
4422                }
4423                other => {
4424                    return Err(self.error(&format!(
4425                        "unknown scope field `{other}` in scope `{}` — expected \
4426                         `targets` / `depth` / `approver`",
4427                        node.name
4428                    )))
4429                }
4430            }
4431            if self.check(TokenType::Comma) {
4432                self.consume(TokenType::Comma)?;
4433            }
4434        }
4435        self.consume(TokenType::RBrace)?;
4436        Ok(node)
4437    }
4438
4439    fn parse_quant(&mut self) -> Result<QuantBlock, ParseError> {
4440        let tok = self.current().clone();
4441        self.advance(); // consume `quant`
4442
4443        let mut block = QuantBlock {
4444            encoding: None,
4445            observable: None,
4446            qubits: None,
4447            depth: None,
4448            bandwidth: None,
4449            reupload: None,
4450            // D1/D9 default backend: the CPU simulator effect. `qpu_native` is
4451            // opt-in via `backend: qpu_native`.
4452            effect: "quant_sim".to_string(),
4453            body: Vec::new(),
4454            loc: Loc {
4455                line: tok.line,
4456                column: tok.column,
4457            },
4458        };
4459
4460        // ── Optional attribute header: `(key: value, …)` ──
4461        if self.check(TokenType::LParen) {
4462            self.advance();
4463            while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
4464                let key = self.consume_any_ident_or_kw()?.value;
4465                self.consume(TokenType::Colon)?;
4466                match key.as_str() {
4467                    "encoding" => {
4468                        block.encoding = Some(self.consume_any_ident_or_kw()?.value)
4469                    }
4470                    "observable" => {
4471                        block.observable = Some(self.parse_dotted_identifier()?)
4472                    }
4473                    "qubits" => block.qubits = Some(self.consume_number()? as i64),
4474                    "depth" => block.depth = Some(self.consume_number()? as i64),
4475                    "bandwidth" => block.bandwidth = Some(self.consume_number()?),
4476                    // §Fase 69.c — data re-uploading layers.
4477                    "reupload" => block.reupload = Some(self.consume_number()? as i64),
4478                    // `backend:` selects the algebraic-effect tag (D1/D9).
4479                    "backend" => block.effect = self.consume_any_ident_or_kw()?.value,
4480                    other => {
4481                        return Err(ParseError {
4482                            message: format!(
4483                                "Unknown `quant` attribute `{other}` — expected one of \
4484                                 encoding, observable, qubits, depth, bandwidth, reupload, backend"
4485                            ),
4486                            line: self.current().line,
4487                            column: self.current().column,
4488                            ..Default::default()
4489                        });
4490                    }
4491                }
4492                // Optional comma between attributes (order-free, trailing-comma ok).
4493                if self.check(TokenType::Comma) {
4494                    self.advance();
4495                }
4496            }
4497            self.consume(TokenType::RParen)?;
4498        }
4499
4500        // ── Body: real nested flow steps (like `par`) ──
4501        self.consume(TokenType::LBrace)?;
4502        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4503            block.body.push(self.parse_flow_step()?);
4504        }
4505        self.consume(TokenType::RBrace)?;
4506
4507        Ok(block)
4508    }
4509
4510    /// §Fase 51.d.2 — Parse the `yield <expr>` measurement point. Reuses the
4511    /// `let`-value expression grammar (reference / literal / arithmetic) so the
4512    /// yielded value's tokenization intent is preserved in `value_kind`.
4513    fn parse_yield(&mut self) -> Result<YieldStatement, ParseError> {
4514        let tok = self.consume(TokenType::Yield)?;
4515        let loc = self.loc_of(&tok);
4516        self.last_let_value_kind = "literal".to_string();
4517        let value_expr = self.parse_let_value_expr()?;
4518        Ok(YieldStatement {
4519            value_expr,
4520            value_kind: self.last_let_value_kind.clone(),
4521            loc,
4522        })
4523    }
4524
4525    /// Parse: keyword Name on target -> output_type (apply pattern).
4526    /// §Fase 111.f — `compute <Name> on <a>, <b>, … -> <out>`.
4527    ///
4528    /// Positional arguments, bound to the compute's declared parameters in order.
4529    /// The generic [`Self::parse_apply_step`] captured a single `on <target>` and
4530    /// then the call site threw even that away (`arguments: Vec::new()`).
4531    fn parse_compute_apply(&mut self) -> Result<ComputeApplyStep, ParseError> {
4532        let tok = self.current().clone();
4533        let loc = self.loc_of(&tok);
4534        self.advance(); // consume `compute`
4535        let compute_name = self.consume_any_ident_or_kw()?.value.clone();
4536
4537        let mut arguments = Vec::new();
4538        if self.current().value == "on" {
4539            self.advance();
4540            loop {
4541                arguments.push(self.consume_any_ident_or_kw()?.value.clone());
4542                if self.check(TokenType::Comma) {
4543                    self.advance();
4544                } else {
4545                    break;
4546                }
4547            }
4548        }
4549
4550        let mut output_name = String::new();
4551        if self.check(TokenType::Arrow) {
4552            self.advance();
4553            output_name = self.consume_any_ident_or_kw()?.value.clone();
4554        }
4555
4556        Ok(ComputeApplyStep {
4557            compute_name,
4558            arguments,
4559            output_name,
4560            loc,
4561        })
4562    }
4563
4564    fn parse_apply_step(&mut self, _kw: &str) -> Result<(Loc, String, String, String), ParseError> {
4565        let tok = self.current().clone();
4566        self.advance(); // consume keyword
4567        let name = self.consume_any_ident_or_kw()?.value.clone();
4568        let mut target = String::new();
4569        let mut output_type = String::new();
4570        // "on" target
4571        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4572            let next = self.current().clone();
4573            if next.value == "on" {
4574                self.advance();
4575                target = self.consume_any_ident_or_kw()?.value.clone();
4576            }
4577        }
4578        // -> output_type
4579        if self.check(TokenType::Arrow) {
4580            self.advance();
4581            output_type = self.consume_any_ident_or_kw()?.value.clone();
4582        }
4583        // Skip optional braced block
4584        if self.check(TokenType::LBrace) {
4585            self.skip_braced_block()?;
4586        }
4587        Ok((
4588            Loc {
4589                line: tok.line,
4590                column: tok.column,
4591            },
4592            name,
4593            target,
4594            output_type,
4595        ))
4596    }
4597
4598    fn parse_weave_step(&mut self) -> Result<FlowStep, ParseError> {
4599        let tok = self.current().clone();
4600        self.advance();
4601        let mut node = WeaveStep {
4602            sources: Vec::new(),
4603            target: String::new(),
4604            format_type: String::new(),
4605            priority: Vec::new(),
4606            style: String::new(),
4607            loc: Loc {
4608                line: tok.line,
4609                column: tok.column,
4610            },
4611        };
4612        if self.check(TokenType::LBrace) {
4613            self.advance();
4614            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4615                let f = self.current().value.clone();
4616                self.advance();
4617                if self.check(TokenType::Colon) {
4618                    self.advance();
4619                    match f.as_str() {
4620                        "sources" => node.sources = self.parse_bracketed_identifiers()?,
4621                        "target" => node.target = self.consume_any_ident_or_kw()?.value.clone(),
4622                        "format" => {
4623                            node.format_type = self.consume_any_ident_or_kw()?.value.clone()
4624                        }
4625                        "priority" => node.priority = self.parse_bracketed_identifiers()?,
4626                        "style" => node.style = self.consume_any_ident_or_kw()?.value.clone(),
4627                        _ => self.skip_value(),
4628                    }
4629                }
4630            }
4631            if self.check(TokenType::RBrace) {
4632                self.advance();
4633            }
4634        }
4635        Ok(FlowStep::Weave(node))
4636    }
4637
4638    fn parse_use_step(&mut self) -> Result<FlowStep, ParseError> {
4639        let tok = self.current().clone();
4640        self.advance();
4641        let tool_name = self.consume_any_ident_or_kw()?.value.clone();
4642        // §Fase 58.b — two mutually-exclusive `use` argument surfaces:
4643        //   * `use Tool(query = "${q}", max_results = 5)` — D2 canonical
4644        //     multi-field keyword args (§58.b `UseArgs::Named`).
4645        //   * `use Tool on "${arg}"` / `on query` — the §54.b single positional
4646        //     argument (D5 back-compat, `UseArgs::LegacyPositional`):
4647        //       - a STRING LITERAL carrying interpolation (`on "${query}"`)
4648        //         resolved at dispatch against request-bound flow params;
4649        //       - a BARE identifier / literal (`on query` / `on 42`) verbatim.
4650        //     (Unquoted `${query}` is intentionally NOT a form — interpolation
4651        //     lives inside string literals everywhere in Axon.)
4652        let args = if self.check(TokenType::LParen) {
4653            UseArgs::Named(self.parse_named_arg_list()?)
4654        } else {
4655            let mut argument = String::new();
4656            if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4657                let next = self.current().clone();
4658                if next.value == "on" {
4659                    self.advance();
4660                    argument = self.consume_any_ident_or_kw()?.value.clone();
4661                }
4662            }
4663            UseArgs::LegacyPositional(argument)
4664        };
4665        if self.check(TokenType::LBrace) {
4666            self.skip_braced_block()?;
4667        }
4668        Ok(FlowStep::UseTool(UseToolStep {
4669            tool_name,
4670            args,
4671            loc: Loc {
4672                line: tok.line,
4673                column: tok.column,
4674            },
4675        }))
4676    }
4677
4678    /// §Fase 58.b — parse `(name = value, …)` keyword args for the canonical
4679    /// `use Tool(...)` multi-field dispatch. Values are captured as expression
4680    /// strings (StringLit / Integer / Float / Bool / dotted identifier / list)
4681    /// via the shared `parse_let_atom`, since the frontend has no structured
4682    /// `Expr`. A trailing comma is tolerated; `()` yields no args.
4683    fn parse_named_arg_list(&mut self) -> Result<Vec<(String, String, String)>, ParseError> {
4684        self.consume(TokenType::LParen)?;
4685        let mut args = Vec::new();
4686        while !self.check(TokenType::RParen) {
4687            // Accept a keyword-as-name (`filter`, `type`, `from`, …) — real
4688            // adopter schemas use such names; the following `=` disambiguates.
4689            let name = self.consume_any_ident_or_kw()?.value;
4690            self.consume(TokenType::Assign)?;
4691            let value = self.parse_let_atom()?;
4692            // §Fase 60 — `parse_let_atom` classified the value (`"literal"` vs
4693            // `"reference"`); carry it so the runtime resolves a bare
4694            // identifier / `Step.output` as a binding lookup, not a literal.
4695            let value_kind = self.last_let_value_kind.clone();
4696            args.push((name, value, value_kind));
4697            if self.check(TokenType::Comma) {
4698                self.advance();
4699            } else {
4700                break;
4701            }
4702        }
4703        self.consume(TokenType::RParen)?;
4704        Ok(args)
4705    }
4706
4707    fn parse_remember_step(&mut self) -> Result<FlowStep, ParseError> {
4708        let tok = self.current().clone();
4709        self.advance();
4710        let expr = self.consume_any_ident_or_kw()?.value.clone();
4711        let mut mem = String::new();
4712        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4713            let next = self.current().clone();
4714            if next.value == "in" || next.ttype == TokenType::In {
4715                self.advance();
4716                mem = self.consume_any_ident_or_kw()?.value.clone();
4717            }
4718        }
4719        Ok(FlowStep::Remember(RememberStep {
4720            expression: expr,
4721            memory_target: mem,
4722            loc: Loc {
4723                line: tok.line,
4724                column: tok.column,
4725            },
4726        }))
4727    }
4728
4729    fn parse_recall_step(&mut self) -> Result<FlowStep, ParseError> {
4730        let tok = self.current().clone();
4731        self.advance();
4732        let query = if self.check(TokenType::StringLit) {
4733            self.consume(TokenType::StringLit)?.value.clone()
4734        } else {
4735            self.consume_any_ident_or_kw()?.value.clone()
4736        };
4737        let mut mem = String::new();
4738        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4739            let next = self.current().clone();
4740            if next.value == "from" || next.ttype == TokenType::From {
4741                self.advance();
4742                mem = self.consume_any_ident_or_kw()?.value.clone();
4743            }
4744        }
4745        Ok(FlowStep::Recall(RecallStep {
4746            query,
4747            memory_source: mem,
4748            loc: Loc {
4749                line: tok.line,
4750                column: tok.column,
4751            },
4752        }))
4753    }
4754
4755    fn parse_hibernate_step(&mut self) -> Result<FlowStep, ParseError> {
4756        let tok = self.current().clone();
4757        self.advance();
4758        let mut event = String::new();
4759        let mut timeout = String::new();
4760        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4761            event = self.consume_any_ident_or_kw()?.value.clone();
4762        }
4763        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4764            let next = self.current().clone();
4765            if next.ttype == TokenType::Duration {
4766                self.advance();
4767                timeout = next.value.clone();
4768            }
4769        }
4770        Ok(FlowStep::Hibernate(HibernateStep {
4771            event_name: event,
4772            timeout,
4773            loc: Loc {
4774                line: tok.line,
4775                column: tok.column,
4776            },
4777        }))
4778    }
4779
4780    /// §Fase 108.d — `focus <Dataspace> { where: "<filter>", select: [cols], as: <name> }`
4781    /// — σ_φ ∘ π_v over a declared dataspace. The `where:` string is the
4782    /// §35 data-plane filter grammar (D108.9, shared with retrieve /
4783    /// navigate). Pre-108.d the optional body was silently discarded.
4784    /// §Fase 109.a — `grad <letName> wrt <x> [as <name>]` /
4785    /// `grad <letName> wrt [a, b] as <name>`. The differentiation itself
4786    /// happens at CHECK/IR time (T931/T932 + the symbolic differentiator);
4787    /// the parser only captures the surface.
4788    fn parse_grad_step(&mut self) -> Result<FlowStep, ParseError> {
4789        let tok = self.current().clone();
4790        self.advance();
4791        let target = self.consume_any_ident_or_kw()?.value.clone();
4792        let mut wrt: Vec<String> = Vec::new();
4793        let mut output = String::new();
4794        if !self.at_declaration_start() && self.current().value == "wrt" {
4795            self.advance();
4796            if self.check(TokenType::LBracket) {
4797                wrt = self.parse_bracketed_identifiers()?;
4798            } else {
4799                wrt.push(self.consume_any_ident_or_kw()?.value.clone());
4800            }
4801        }
4802        if !self.at_declaration_start() && self.current().value == "as" {
4803            self.advance();
4804            output = self.consume_any_ident_or_kw()?.value.clone();
4805        }
4806        Ok(FlowStep::Grad(GradStep {
4807            target,
4808            wrt,
4809            output,
4810            loc: Loc {
4811                line: tok.line,
4812                column: tok.column,
4813            },
4814        }))
4815    }
4816
4817    fn parse_focus_step(&mut self) -> Result<FlowStep, ParseError> {
4818        let tok = self.current().clone();
4819        self.advance();
4820        let expression = if self.at_declaration_start()
4821            || self.check(TokenType::RBrace)
4822            || self.check(TokenType::Eof)
4823        {
4824            String::new()
4825        } else {
4826            self.consume_any_ident_or_kw()?.value.clone()
4827        };
4828        let mut where_expr = String::new();
4829        let mut select: Vec<String> = Vec::new();
4830        let mut output = String::new();
4831        if self.check(TokenType::LBrace) {
4832            self.advance();
4833            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4834                if self.check(TokenType::Comma) {
4835                    self.advance();
4836                    continue;
4837                }
4838                let f = self.current().value.clone();
4839                self.advance();
4840                if self.check(TokenType::Colon) {
4841                    self.advance();
4842                    match f.as_str() {
4843                        "where" => {
4844                            where_expr = self.consume(TokenType::StringLit)?.value.clone()
4845                        }
4846                        "select" => select = self.parse_bracketed_identifiers()?,
4847                        "as" | "alias" => {
4848                            output = self.consume_any_ident_or_kw()?.value.clone()
4849                        }
4850                        _ => self.skip_value(),
4851                    }
4852                }
4853            }
4854            if self.check(TokenType::RBrace) {
4855                self.advance();
4856            }
4857        }
4858        Ok(FlowStep::Focus(FocusStep {
4859            expression,
4860            where_expr,
4861            select,
4862            output,
4863            loc: Loc {
4864                line: tok.line,
4865                column: tok.column,
4866            },
4867        }))
4868    }
4869
4870    fn parse_associate_step(&mut self) -> Result<FlowStep, ParseError> {
4871        let tok = self.current().clone();
4872        self.advance();
4873        let left = self.consume_any_ident_or_kw()?.value.clone();
4874        let mut right = String::new();
4875        let mut using = String::new();
4876        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4877            right = self.consume_any_ident_or_kw()?.value.clone();
4878        }
4879        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4880            let next = self.current().clone();
4881            if next.value == "using" {
4882                self.advance();
4883                using = self.consume_any_ident_or_kw()?.value.clone();
4884            }
4885        }
4886        let mut output = String::new();
4887        if self.check(TokenType::LBrace) {
4888            self.advance();
4889            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4890                let f = self.current().value.clone();
4891                self.advance();
4892                if self.check(TokenType::Colon) {
4893                    self.advance();
4894                    match f.as_str() {
4895                        "as" | "alias" => output = self.consume_any_ident_or_kw()?.value.clone(),
4896                        _ => self.skip_value(),
4897                    }
4898                }
4899            }
4900            if self.check(TokenType::RBrace) {
4901                self.advance();
4902            }
4903        }
4904        Ok(FlowStep::Associate(AssociateStep {
4905            left,
4906            right,
4907            using_field: using,
4908            output,
4909            loc: Loc {
4910                line: tok.line,
4911                column: tok.column,
4912            },
4913        }))
4914    }
4915
4916    fn parse_aggregate_step(&mut self) -> Result<FlowStep, ParseError> {
4917        let tok = self.current().clone();
4918        self.advance();
4919        let target = self.consume_any_ident_or_kw()?.value.clone();
4920        let mut group_by = Vec::new();
4921        let mut alias = String::new();
4922        let mut compute: Vec<String> = Vec::new();
4923        let mut where_expr = String::new();
4924        if self.check(TokenType::LBrace) {
4925            self.advance();
4926            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4927                let f = self.current().value.clone();
4928                self.advance();
4929                if self.check(TokenType::Colon) {
4930                    self.advance();
4931                    match f.as_str() {
4932                        "group_by" => group_by = self.parse_bracketed_identifiers()?,
4933                        "alias" | "as" => alias = self.consume_any_ident_or_kw()?.value.clone(),
4934                        // §Fase 108.d — the closed aggregate catalog, kept
4935                        // RAW (`count`, `sum(score)`, …); T930 validates.
4936                        "compute" => compute = self.parse_bracketed_aggregates()?,
4937                        // §Fase 108.d — the data-plane where (D108.9).
4938                        "where" => where_expr = self.consume(TokenType::StringLit)?.value.clone(),
4939                        _ => self.skip_value(),
4940                    }
4941                }
4942            }
4943            if self.check(TokenType::RBrace) {
4944                self.advance();
4945            }
4946        }
4947        Ok(FlowStep::Aggregate(AggregateStep {
4948            target,
4949            group_by,
4950            alias,
4951            compute,
4952            where_expr,
4953            loc: Loc {
4954                line: tok.line,
4955                column: tok.column,
4956            },
4957        }))
4958    }
4959
4960    fn parse_explore_step(&mut self) -> Result<FlowStep, ParseError> {
4961        let tok = self.current().clone();
4962        self.advance();
4963        let target = self.consume_any_ident_or_kw()?.value.clone();
4964        let mut limit = None;
4965        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4966            if self.current().ttype == TokenType::Integer {
4967                limit = self.current().value.parse::<i64>().ok();
4968                self.advance();
4969            }
4970        }
4971        let mut output = String::new();
4972        if self.check(TokenType::LBrace) {
4973            self.advance();
4974            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4975                let f = self.current().value.clone();
4976                self.advance();
4977                if self.check(TokenType::Colon) {
4978                    self.advance();
4979                    match f.as_str() {
4980                        "as" | "alias" => output = self.consume_any_ident_or_kw()?.value.clone(),
4981                        _ => self.skip_value(),
4982                    }
4983                }
4984            }
4985            if self.check(TokenType::RBrace) {
4986                self.advance();
4987            }
4988        }
4989        Ok(FlowStep::ExploreStep(ExploreStepNode {
4990            target,
4991            limit,
4992            output,
4993            loc: Loc {
4994                line: tok.line,
4995                column: tok.column,
4996            },
4997        }))
4998    }
4999
5000    /// §Fase 108.d — parse `[count, sum(score), avg(x)]`: bracketed
5001    /// aggregate entries, each `ident` or `ident(ident)`, kept raw.
5002    fn parse_bracketed_aggregates(&mut self) -> Result<Vec<String>, ParseError> {
5003        let mut out = Vec::new();
5004        self.consume(TokenType::LBracket)?;
5005        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
5006            let name = self.consume_any_ident_or_kw()?.value.clone();
5007            if self.check(TokenType::LParen) {
5008                self.advance();
5009                let col = self.consume_any_ident_or_kw()?.value.clone();
5010                self.consume(TokenType::RParen)?;
5011                out.push(format!("{name}({col})"));
5012            } else {
5013                out.push(name);
5014            }
5015            if self.check(TokenType::Comma) {
5016                self.advance();
5017            }
5018        }
5019        self.consume(TokenType::RBracket)?;
5020        Ok(out)
5021    }
5022
5023    /// §Fase 108.c — the governed ingest step:
5024    ///
5025    /// ```text
5026    /// ingest <sourceRef> into <Dataspace> {
5027    ///     format: csv | json
5028    ///     limits { max_bytes: N, max_rows: N }
5029    /// }
5030    /// ```
5031    ///
5032    /// Until 108.c the body was consumed by `skip_braced_block()`. Now it
5033    /// is a closed grammar: `format:` (raw here; required + validated by
5034    /// `axon-T929`) and an optional `limits { … }` block whose bounds are
5035    /// enforced on the raw byte stream BEFORE parsing (§100). An unknown
5036    /// body entry is a parse error.
5037    fn parse_ingest_step(&mut self) -> Result<FlowStep, ParseError> {
5038        let tok = self.current().clone();
5039        self.advance();
5040        let source = self.consume_any_ident_or_kw()?.value.clone();
5041        let mut target = String::new();
5042        let mut format = String::new();
5043        let mut max_bytes: Option<u64> = None;
5044        let mut max_rows: Option<u64> = None;
5045        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
5046            let next = self.current().clone();
5047            if next.value == "into" || next.ttype == TokenType::Into {
5048                self.advance();
5049                target = self.consume_any_ident_or_kw()?.value.clone();
5050            }
5051        }
5052        if self.check(TokenType::LBrace) {
5053            self.consume(TokenType::LBrace)?;
5054            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5055                // Optional separators between body entries.
5056                if self.check(TokenType::Comma) {
5057                    self.advance();
5058                    continue;
5059                }
5060                let entry = self.current().clone();
5061                match entry.value.as_str() {
5062                    "format" => {
5063                        self.advance();
5064                        self.consume(TokenType::Colon)?;
5065                        format = self.consume_any_ident_or_kw()?.value.clone();
5066                    }
5067                    "limits" => {
5068                        self.advance();
5069                        self.consume(TokenType::LBrace)?;
5070                        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5071                            let bound = self.current().clone();
5072                            self.advance();
5073                            self.consume(TokenType::Colon)?;
5074                            let num_tok = self.consume(TokenType::Integer)?.clone();
5075                            let value = num_tok.value.parse::<u64>().map_err(|_| ParseError {
5076                                message: format!(
5077                                    "ingest `limits` bound `{}` must be a non-negative \
5078                                     integer byte/row count, got `{}`.",
5079                                    bound.value, num_tok.value
5080                                ),
5081                                line: num_tok.line,
5082                                column: num_tok.column,
5083                                ..Default::default()
5084                            })?;
5085                            match bound.value.as_str() {
5086                                "max_bytes" => max_bytes = Some(value),
5087                                "max_rows" => max_rows = Some(value),
5088                                other => {
5089                                    return Err(ParseError {
5090                                        message: format!(
5091                                            "Unknown ingest limit `{other}`. The closed \
5092                                             limits grammar is `max_bytes: <N>` and \
5093                                             `max_rows: <N>` — bounds enforced on the raw \
5094                                             stream BEFORE parsing (§100).",
5095                                        ),
5096                                        line: bound.line,
5097                                        column: bound.column,
5098                                        ..Default::default()
5099                                    });
5100                                }
5101                            }
5102                            if self.check(TokenType::Comma) {
5103                                self.advance();
5104                            }
5105                        }
5106                        self.consume(TokenType::RBrace)?;
5107                    }
5108                    other => {
5109                        return Err(ParseError {
5110                            message: format!(
5111                                "Unknown entry `{other}` in ingest body. The closed \
5112                                 grammar is `format: csv|json` and \
5113                                 `limits {{ max_bytes: <N>, max_rows: <N> }}`.",
5114                            ),
5115                            line: entry.line,
5116                            column: entry.column,
5117                            ..Default::default()
5118                        });
5119                    }
5120                }
5121            }
5122            self.consume(TokenType::RBrace)?;
5123        }
5124        Ok(FlowStep::Ingest(IngestStep {
5125            source,
5126            target,
5127            format,
5128            max_bytes,
5129            max_rows,
5130            loc: Loc {
5131                line: tok.line,
5132                column: tok.column,
5133            },
5134        }))
5135    }
5136
5137    fn parse_navigate_step(&mut self) -> Result<FlowStep, ParseError> {
5138        let tok = self.current().clone();
5139        self.advance();
5140        let pix_name = self.consume_any_ident_or_kw()?.value.clone();
5141        let mut node = NavigateStep {
5142            pix_name,
5143            corpus_name: String::new(),
5144            query_expr: String::new(),
5145            trail_enabled: false,
5146            output_name: String::new(),
5147            seed: String::new(),
5148            budget: None,
5149            where_expr: String::new(),
5150            loc: Loc {
5151                line: tok.line,
5152                column: tok.column,
5153            },
5154        };
5155        if self.check(TokenType::LBrace) {
5156            self.advance();
5157            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5158                let f = self.current().value.clone();
5159                self.advance();
5160                if self.check(TokenType::Colon) {
5161                    self.advance();
5162                    match f.as_str() {
5163                        "corpus" => {
5164                            node.corpus_name = self.consume_any_ident_or_kw()?.value.clone()
5165                        }
5166                        "query" => {
5167                            node.query_expr = self.consume(TokenType::StringLit)?.value.clone()
5168                        }
5169                        "trail" => {
5170                            node.trail_enabled = self.consume_any_ident_or_kw()?.value == "true"
5171                        }
5172                        "output" | "as" => {
5173                            node.output_name = self.consume_any_ident_or_kw()?.value.clone()
5174                        }
5175                        // §Fase 63.B — MDN corpus-graph navigation.
5176                        "from" => node.seed = self.consume_any_ident_or_kw()?.value.clone(),
5177                        "budget" => node.budget = self.parse_optional_int(),
5178                        // §Fase 66 (Q2) — column-scoped navigation: a raw filter
5179                        // expr (mirrors `retrieve … where`) pushed to the SELECT
5180                        // that sources the corpus `documents:`/`relations:` rows,
5181                        // so a `corpus from axonstore` is scoped to a sub-tenant
5182                        // COLUMN (`where: "tenant_id == '${tenant_id}'"`), not just
5183                        // the axon-tenant RLS scope. Resolved by the §37.d filter
5184                        // compiler at runtime (`${name}` → `$N` bind params).
5185                        "where" => {
5186                            node.where_expr = self.consume(TokenType::StringLit)?.value.clone()
5187                        }
5188                        _ => self.skip_value(),
5189                    }
5190                }
5191            }
5192            if self.check(TokenType::RBrace) {
5193                self.advance();
5194            }
5195        }
5196        Ok(FlowStep::Navigate(node))
5197    }
5198
5199    fn parse_drill_step(&mut self) -> Result<FlowStep, ParseError> {
5200        let tok = self.current().clone();
5201        self.advance();
5202        let pix_name = self.consume_any_ident_or_kw()?.value.clone();
5203        let mut node = DrillStep {
5204            pix_name,
5205            subtree_path: String::new(),
5206            query_expr: String::new(),
5207            output_name: String::new(),
5208            loc: Loc {
5209                line: tok.line,
5210                column: tok.column,
5211            },
5212        };
5213        if self.check(TokenType::LBrace) {
5214            self.advance();
5215            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5216                let f = self.current().value.clone();
5217                self.advance();
5218                if self.check(TokenType::Colon) {
5219                    self.advance();
5220                    match f.as_str() {
5221                        "subtree" | "path" => {
5222                            node.subtree_path = self.consume(TokenType::StringLit)?.value.clone()
5223                        }
5224                        "query" => {
5225                            node.query_expr = self.consume(TokenType::StringLit)?.value.clone()
5226                        }
5227                        "output" | "as" => {
5228                            node.output_name = self.consume_any_ident_or_kw()?.value.clone()
5229                        }
5230                        _ => self.skip_value(),
5231                    }
5232                }
5233            }
5234            if self.check(TokenType::RBrace) {
5235                self.advance();
5236            }
5237        }
5238        Ok(FlowStep::Drill(node))
5239    }
5240
5241    fn parse_corroborate_step(&mut self) -> Result<FlowStep, ParseError> {
5242        let tok = self.current().clone();
5243        self.advance();
5244        let nav_ref = self.consume_any_ident_or_kw()?.value.clone();
5245        let mut output = String::new();
5246        if self.check(TokenType::Arrow) {
5247            self.advance();
5248            output = self.consume_any_ident_or_kw()?.value.clone();
5249        }
5250        Ok(FlowStep::Corroborate(CorroborateStep {
5251            navigate_ref: nav_ref,
5252            output_name: output,
5253            loc: Loc {
5254                line: tok.line,
5255                column: tok.column,
5256            },
5257        }))
5258    }
5259
5260    fn parse_listen_step(&mut self) -> Result<FlowStep, ParseError> {
5261        let tok = self.current().clone();
5262        self.advance();
5263        // §λ-L-E Fase 13 D4 — dual-mode listen:
5264        //   • String topic (legacy, deprecated since Fase 13)
5265        //   • Identifier (canonical: declared ChannelDefinition)
5266        let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
5267            (self.consume(TokenType::StringLit)?.value.clone(), false)
5268        } else {
5269            (self.consume_any_ident_or_kw()?.value.clone(), true)
5270        };
5271        let mut alias = String::new();
5272        if !self.at_declaration_start()
5273            && !self.check(TokenType::RBrace)
5274            && !self.check(TokenType::LBrace)
5275        {
5276            let next = self.current().clone();
5277            if next.value == "as" || next.ttype == TokenType::As {
5278                self.advance();
5279                alias = self.consume_any_ident_or_kw()?.value.clone();
5280            }
5281        }
5282        // §Fase 52.a — parse the handler body into real flow-steps (was
5283        // `skip_braced_block`'d, leaving the listener inert). The body runs on
5284        // each event / scheduled tick.
5285        let body = self.parse_listener_body()?;
5286        Ok(FlowStep::Listen(ListenStep {
5287            channel,
5288            channel_is_ref,
5289            event_alias: alias,
5290            body,
5291            loc: Loc {
5292                line: tok.line,
5293                column: tok.column,
5294            },
5295        }))
5296    }
5297
5298    /// §Fase 52.a — parse a `listen … { <flow steps> }` handler body. The body
5299    /// is OPTIONAL (a bodyless `listen channel` returns an empty Vec); when
5300    /// present, each statement is a real [`FlowStep`] (the same grammar as a
5301    /// flow / `quant` / `par` body), executed per trigger by the §52.c runtime.
5302    fn parse_listener_body(&mut self) -> Result<Vec<FlowStep>, ParseError> {
5303        let mut body = Vec::new();
5304        if self.check(TokenType::LBrace) {
5305            self.advance(); // consume `{`
5306            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5307                body.push(self.parse_flow_step()?);
5308            }
5309            self.consume(TokenType::RBrace)?;
5310        }
5311        Ok(body)
5312    }
5313
5314    fn parse_retrieve_step(&mut self) -> Result<FlowStep, ParseError> {
5315        let tok = self.current().clone();
5316        self.advance();
5317        let store = self.consume_any_ident_or_kw()?.value.clone();
5318        let mut where_expr = String::new();
5319        let mut alias = String::new();
5320        let mut order_by = String::new();
5321        let mut limit_expr = String::new();
5322        let mut aggregate = String::new();
5323        let mut group_by = String::new();
5324        let mut cache = String::new();
5325        if self.check(TokenType::LBrace) {
5326            self.advance();
5327            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5328                let f = self.current().value.clone();
5329                self.advance();
5330                if self.check(TokenType::Colon) {
5331                    self.advance();
5332                    match f.as_str() {
5333                        "where" => where_expr = self.consume(TokenType::StringLit)?.value.clone(),
5334                        "as" | "alias" => alias = self.consume_any_ident_or_kw()?.value.clone(),
5335                        // §Fase 67.b — `order_by:` is a string literal
5336                        // (`"col asc, col2 desc"`), same surface as `where:`.
5337                        "order_by" => {
5338                            order_by = self.consume(TokenType::StringLit)?.value.clone()
5339                        }
5340                        // §Fase 67.b — `limit:` is a bare integer literal
5341                        // (`limit: 100`) OR a string carrying a binding
5342                        // (`limit: "${max}"`). Captured raw; the runtime
5343                        // resolves + validates it as a `u32`.
5344                        "limit" => {
5345                            let t = self.current().clone();
5346                            match t.ttype {
5347                                TokenType::Integer | TokenType::StringLit => {
5348                                    limit_expr = t.value.clone();
5349                                    self.advance();
5350                                }
5351                                _ => self.skip_value(),
5352                            }
5353                        }
5354                        // §Fase 76.d — `aggregate:` is a string literal from
5355                        // the CLOSED catalog (`"count"`, `"sum(tokens)"`, …);
5356                        // `group_by:` is a string literal listing columns
5357                        // (`"industry, status"`). Both captured raw; the
5358                        // §38.d proof (axon-T843/T844/T845) + the runtime
5359                        // (`filter::parse_aggregate_clause`) validate.
5360                        "aggregate" => {
5361                            aggregate = self.consume(TokenType::StringLit)?.value.clone()
5362                        }
5363                        "group_by" => {
5364                            group_by = self.consume(TokenType::StringLit)?.value.clone()
5365                        }
5366                        // §Fase 85.b — `cache:` names a declared `cache`
5367                        // policy. A retrieve reads a store (never `pure`), so
5368                        // caching it always accepts staleness — the checker
5369                        // requires a finite `ttl:` on the referenced cache
5370                        // (axon-T865) and resolves the reference (axon-T864).
5371                        "cache" => cache = self.consume_any_ident_or_kw()?.value.clone(),
5372                        _ => self.skip_value(),
5373                    }
5374                }
5375            }
5376            if self.check(TokenType::RBrace) {
5377                self.advance();
5378            }
5379        }
5380        Ok(FlowStep::Retrieve(RetrieveStep {
5381            store_name: store,
5382            where_expr,
5383            alias,
5384            order_by,
5385            limit_expr,
5386            aggregate,
5387            group_by,
5388            cache,
5389            loc: Loc {
5390                line: tok.line,
5391                column: tok.column,
5392            },
5393        }))
5394    }
5395
5396    /// §Fase 35.m — Parse a `purge` step, capturing the optional
5397    /// `{ where: "<expr>" }` filter. (Fase 35.p moved `mutate` to its
5398    /// own `parse_mutate_step`, which also captures SET columns; this
5399    /// helper now serves `purge` alone — a `DELETE` has no SET clause.)
5400    ///
5401    /// Before Fase 35.m these two steps parsed via `parse_flow_step_simple`,
5402    /// which *skipped* the braced block — so a written `where:` clause
5403    /// was silently dropped and every `mutate`/`purge` ran against the
5404    /// whole store, leaving the entire Fase 35.b/c parameterized-filter
5405    /// machinery unreachable for them. This mirror of `parse_retrieve_step`
5406    /// (minus the `as:` alias — a mutate/purge binds no result) closes
5407    /// that gap. Returns `(loc, store_name, where_expr)`.
5408    fn parse_store_where_step(
5409        &mut self,
5410    ) -> Result<(Loc, String, String), ParseError> {
5411        let tok = self.current().clone();
5412        self.advance(); // consume the keyword
5413        let store = if self.at_declaration_start()
5414            || self.check(TokenType::RBrace)
5415            || self.check(TokenType::Eof)
5416        {
5417            String::new()
5418        } else {
5419            self.consume_any_ident_or_kw()?.value.clone()
5420        };
5421        let mut where_expr = String::new();
5422        if self.check(TokenType::LBrace) {
5423            self.advance();
5424            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5425                let field = self.current().value.clone();
5426                self.advance();
5427                if self.check(TokenType::Colon) {
5428                    self.advance();
5429                    match field.as_str() {
5430                        "where" => {
5431                            where_expr =
5432                                self.consume(TokenType::StringLit)?.value.clone()
5433                        }
5434                        _ => self.skip_value(),
5435                    }
5436                }
5437            }
5438            if self.check(TokenType::RBrace) {
5439                self.advance();
5440            }
5441        }
5442        Ok((
5443            Loc {
5444                line: tok.line,
5445                column: tok.column,
5446            },
5447            store,
5448            where_expr,
5449        ))
5450    }
5451
5452    /// §Fase 35.o — Parse a `persist` step, capturing the optional
5453    /// `{ col: value }` field block.
5454    ///
5455    /// Before Fase 35.o `persist` parsed via `parse_flow_step_simple`,
5456    /// which *skipped* the braced block — so a written field block was
5457    /// silently dropped and the runtime fell back to writing every
5458    /// context binding as a row, which fails against any real table
5459    /// (flows always carry more bindings than a table has columns).
5460    /// This captures the declared columns into `PersistStep.fields`;
5461    /// the runtime writes exactly those (interpolated). A `persist`
5462    /// with no block keeps the v1.30.0 user-bindings fallback — fully
5463    /// backward-compatible. Mirror of `parse_retrieve_step`, but the
5464    /// keys are arbitrary column names rather than the fixed
5465    /// `where:` / `as:` filter keys.
5466    ///
5467    /// The optional `into` connector (`persist into <store>`) is
5468    /// accepted and skipped — before Fase 35.o `into` was captured as
5469    /// the store name.
5470    fn parse_persist_step(&mut self) -> Result<FlowStep, ParseError> {
5471        let tok = self.current().clone();
5472        self.advance(); // consume `persist`
5473        // Optional `into` connector — skip it so the store name that
5474        // follows is not mistaken for the target.
5475        if self.current().value == "into" && !self.check(TokenType::LBrace) {
5476            self.advance();
5477        }
5478        let store = if self.at_declaration_start()
5479            || self.check(TokenType::LBrace)
5480            || self.check(TokenType::RBrace)
5481            || self.check(TokenType::Eof)
5482        {
5483            String::new()
5484        } else {
5485            self.consume_any_ident_or_kw()?.value.clone()
5486        };
5487        let mut fields: Vec<(String, String)> = Vec::new();
5488        if self.check(TokenType::LBrace) {
5489            self.advance();
5490            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5491                let col = self.current().value.clone();
5492                self.advance();
5493                if self.check(TokenType::Colon) {
5494                    self.advance();
5495                    let value = if self.check(TokenType::StringLit) {
5496                        self.consume(TokenType::StringLit)?.value.clone()
5497                    } else if self.check(TokenType::RBrace)
5498                        || self.check(TokenType::Eof)
5499                        || self.check(TokenType::Colon)
5500                    {
5501                        String::new()
5502                    } else {
5503                        let v = self.current().clone();
5504                        self.advance();
5505                        v.value.clone()
5506                    };
5507                    fields.push((col, value));
5508                }
5509            }
5510            if self.check(TokenType::RBrace) {
5511                self.advance();
5512            }
5513        }
5514        Ok(FlowStep::Persist(PersistStep {
5515            store_name: store,
5516            fields,
5517            loc: Loc {
5518                line: tok.line,
5519                column: tok.column,
5520            },
5521        }))
5522    }
5523
5524    /// §Fase 35.p — Parse a `mutate` step, capturing both the
5525    /// `{ where: "<expr>" }` filter AND the `{ col: value }` SET
5526    /// assignments.
5527    ///
5528    /// Before Fase 35.p `mutate` parsed via `parse_store_where_step`,
5529    /// which captured only `where:` and *skipped* every other key — so
5530    /// the runtime built the `UPDATE … SET` clause from every flow
5531    /// binding (params + step results + `let`s), which fails against
5532    /// any real table (`column "X" does not exist`). This closes the
5533    /// gap symmetrically to 35.o's `persist` block: every key other
5534    /// than `where:` is a SET column; a `mutate` with no SET column
5535    /// keeps the v1.31.0 user-bindings fallback. `where:` keeps its
5536    /// string-literal grammar (as in `retrieve` / `purge`).
5537    fn parse_mutate_step(&mut self) -> Result<FlowStep, ParseError> {
5538        let tok = self.current().clone();
5539        self.advance(); // consume `mutate`
5540        let store = if self.at_declaration_start()
5541            || self.check(TokenType::LBrace)
5542            || self.check(TokenType::RBrace)
5543            || self.check(TokenType::Eof)
5544        {
5545            String::new()
5546        } else {
5547            self.consume_any_ident_or_kw()?.value.clone()
5548        };
5549        let mut where_expr = String::new();
5550        let mut fields: Vec<(String, String)> = Vec::new();
5551        if self.check(TokenType::LBrace) {
5552            self.advance();
5553            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5554                let key = self.current().value.clone();
5555                self.advance();
5556                if self.check(TokenType::Colon) {
5557                    self.advance();
5558                    if key == "where" {
5559                        where_expr =
5560                            self.consume(TokenType::StringLit)?.value.clone();
5561                    } else {
5562                        let value = if self.check(TokenType::StringLit) {
5563                            self.consume(TokenType::StringLit)?.value.clone()
5564                        } else if self.check(TokenType::RBrace)
5565                            || self.check(TokenType::Eof)
5566                            || self.check(TokenType::Colon)
5567                        {
5568                            String::new()
5569                        } else {
5570                            let v = self.current().clone();
5571                            self.advance();
5572                            v.value.clone()
5573                        };
5574                        fields.push((key, value));
5575                    }
5576                }
5577            }
5578            if self.check(TokenType::RBrace) {
5579                self.advance();
5580            }
5581        }
5582        Ok(FlowStep::Mutate(MutateStep {
5583            store_name: store,
5584            where_expr,
5585            fields,
5586            loc: Loc {
5587                line: tok.line,
5588                column: tok.column,
5589            },
5590        }))
5591    }
5592
5593    // ── TIER 2 DECLARATIONS ────────────────────────────────────────
5594
5595    fn parse_agent(&mut self) -> Result<AgentDefinition, ParseError> {
5596        let tok = self.consume(TokenType::Agent)?;
5597        let name = self.consume(TokenType::Identifier)?.value;
5598        let mut node = AgentDefinition {
5599            name,
5600            goal: String::new(),
5601            tools: Vec::new(),
5602            memory_ref: String::new(),
5603            strategy: String::new(),
5604            on_stuck: String::new(),
5605            shield_ref: String::new(),
5606            max_iterations: None,
5607            max_tokens: None,
5608            max_time: String::new(),
5609            max_cost: None,
5610            loc: Loc {
5611                line: tok.line,
5612                column: tok.column,
5613            },
5614            leading_trivia: Vec::new(),
5615            trailing_trivia: Vec::new(),
5616        };
5617        // Skip optional parameters/return type before brace
5618        while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
5619            self.advance();
5620        }
5621        self.consume(TokenType::LBrace)?;
5622        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5623            let field = self.current().clone();
5624            let field_name = field.value.clone();
5625            self.advance();
5626            if self.check(TokenType::Colon) {
5627                self.advance();
5628                match field_name.as_str() {
5629                    "goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
5630                    "tools" => node.tools = self.parse_bracketed_identifiers()?,
5631                    "memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
5632                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
5633                    "on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
5634                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
5635                    "max_iterations" => node.max_iterations = self.parse_optional_int(),
5636                    "max_tokens" => node.max_tokens = self.parse_optional_int(),
5637                    "max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
5638                    "max_cost" => node.max_cost = self.parse_optional_float(),
5639                    _ => self.skip_value(),
5640                }
5641            } else if self.check(TokenType::LBrace) {
5642                self.skip_braced_block()?;
5643            }
5644        }
5645        self.consume(TokenType::RBrace)?;
5646        Ok(node)
5647    }
5648
5649    /// §Fase 53 — `extension Name { category: effects|scan, members: [ … ] }`.
5650    /// The parser is permissive on field/category VALUES (validated in
5651    /// §53.c by the type-checker — no-shadowing, category-membership);
5652    /// it only enforces the structural grammar here.
5653    fn parse_extension(&mut self) -> Result<ExtensionDefinition, ParseError> {
5654        let tok = self.consume(TokenType::Extension)?;
5655        let name = self.consume(TokenType::Identifier)?.value;
5656        let mut node = ExtensionDefinition {
5657            name,
5658            category: String::new(),
5659            members: Vec::new(),
5660            loc: Loc {
5661                line: tok.line,
5662                column: tok.column,
5663            },
5664            leading_trivia: Vec::new(),
5665            trailing_trivia: Vec::new(),
5666        };
5667        self.consume(TokenType::LBrace)?;
5668        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5669            let field_name = self.current().value.clone();
5670            self.advance();
5671            if self.check(TokenType::Colon) {
5672                self.advance();
5673                match field_name.as_str() {
5674                    "category" => {
5675                        node.category = self.consume_any_ident_or_kw()?.value.clone()
5676                    }
5677                    "members" => node.members = self.parse_extension_members()?,
5678                    _ => self.skip_value(),
5679                }
5680            } else if self.check(TokenType::LBrace) {
5681                self.skip_braced_block()?;
5682            }
5683        }
5684        self.consume(TokenType::RBrace)?;
5685        Ok(node)
5686    }
5687
5688    /// §Fase 53 — parse `[ "name" [ : { semantics: "…", default_confidence: 0.8 } ], … ]`.
5689    /// Each member is a string literal optionally followed by a metadata
5690    /// block. Trailing/interleaved commas are tolerated.
5691    fn parse_extension_members(&mut self) -> Result<Vec<ExtensionMember>, ParseError> {
5692        let mut members = Vec::new();
5693        self.consume(TokenType::LBracket)?;
5694        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
5695            let name_tok = self.consume(TokenType::StringLit)?;
5696            let mut member = ExtensionMember {
5697                name: name_tok.value.clone(),
5698                semantics: None,
5699                default_confidence: None,
5700                loc: Loc {
5701                    line: name_tok.line,
5702                    column: name_tok.column,
5703                },
5704            };
5705            // Optional `: { semantics: "…", default_confidence: 0.8 }`.
5706            if self.check(TokenType::Colon) {
5707                self.advance();
5708                self.consume(TokenType::LBrace)?;
5709                while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5710                    let mkey = self.current().value.clone();
5711                    self.advance();
5712                    if self.check(TokenType::Colon) {
5713                        self.advance();
5714                        match mkey.as_str() {
5715                            "semantics" => {
5716                                member.semantics =
5717                                    Some(self.consume(TokenType::StringLit)?.value.clone())
5718                            }
5719                            "default_confidence" => {
5720                                member.default_confidence = self.parse_optional_float()
5721                            }
5722                            _ => self.skip_value(),
5723                        }
5724                    }
5725                    if self.check(TokenType::Comma) {
5726                        self.advance();
5727                    }
5728                }
5729                self.consume(TokenType::RBrace)?;
5730            }
5731            members.push(member);
5732            if self.check(TokenType::Comma) {
5733                self.advance();
5734            }
5735        }
5736        self.consume(TokenType::RBracket)?;
5737        Ok(members)
5738    }
5739
5740    /// §Fase 71.a/e — `window <Name> { timezone: "…"  allow: [ {days hours} ]
5741    /// exclude: [ "YYYY-MM-DD", … ]  on_outside: skip|defer|warn }`.
5742    fn parse_window(&mut self) -> Result<WindowDefinition, ParseError> {
5743        let tok = self.consume(TokenType::Window)?;
5744        let name = self.consume(TokenType::Identifier)?.value;
5745        let mut node = WindowDefinition {
5746            name,
5747            timezone: String::new(),
5748            allow: Vec::new(),
5749            exclude: Vec::new(),
5750            on_outside: String::new(),
5751            loc: Loc {
5752                line: tok.line,
5753                column: tok.column,
5754            },
5755            leading_trivia: Vec::new(),
5756            trailing_trivia: Vec::new(),
5757        };
5758        self.consume(TokenType::LBrace)?;
5759        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5760            let field_name = self.consume_any_ident_or_kw()?.value;
5761            self.consume(TokenType::Colon)?;
5762            match field_name.as_str() {
5763                "timezone" => node.timezone = self.consume(TokenType::StringLit)?.value,
5764                "allow" => node.allow = self.parse_window_allow()?,
5765                "exclude" => node.exclude = self.parse_window_exclude()?,
5766                "on_outside" => node.on_outside = self.consume_any_ident_or_kw()?.value,
5767                _ => self.skip_value(),
5768            }
5769        }
5770        self.consume(TokenType::RBrace)?;
5771        Ok(node)
5772    }
5773
5774    /// §Fase 71.a — the `allow: [ { … }, { … } ]` span list.
5775    fn parse_window_allow(&mut self) -> Result<Vec<WindowSpan>, ParseError> {
5776        self.consume(TokenType::LBracket)?;
5777        let mut spans = Vec::new();
5778        if !self.check(TokenType::RBracket) {
5779            spans.push(self.parse_window_span()?);
5780            while self.check(TokenType::Comma) {
5781                self.advance();
5782                if self.check(TokenType::RBracket) {
5783                    break; // trailing comma
5784                }
5785                spans.push(self.parse_window_span()?);
5786            }
5787        }
5788        self.consume(TokenType::RBracket)?;
5789        Ok(spans)
5790    }
5791
5792    /// §Fase 71.e — the `exclude: [ "YYYY-MM-DD", … ]` holiday list (ISO
5793    /// date-string literals; validated for real-calendar-date-ness by the
5794    /// `axon-T826` type check). An empty list / absent field ⇒ no holidays.
5795    fn parse_window_exclude(&mut self) -> Result<Vec<String>, ParseError> {
5796        self.consume(TokenType::LBracket)?;
5797        let mut dates = Vec::new();
5798        if !self.check(TokenType::RBracket) {
5799            dates.push(self.consume(TokenType::StringLit)?.value);
5800            while self.check(TokenType::Comma) {
5801                self.advance();
5802                if self.check(TokenType::RBracket) {
5803                    break; // trailing comma
5804                }
5805                dates.push(self.consume(TokenType::StringLit)?.value);
5806            }
5807        }
5808        self.consume(TokenType::RBracket)?;
5809        Ok(dates)
5810    }
5811
5812    /// §Fase 71.a — one span `{ days: Mon..Fri  hours: 9..18 }`.
5813    fn parse_window_span(&mut self) -> Result<WindowSpan, ParseError> {
5814        let tok = self.consume(TokenType::LBrace)?;
5815        let mut span = WindowSpan {
5816            day_start: String::new(),
5817            day_end: String::new(),
5818            hour_start: 0,
5819            hour_end: 0,
5820            loc: Loc {
5821                line: tok.line,
5822                column: tok.column,
5823            },
5824        };
5825        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5826            let field = self.consume_any_ident_or_kw()?.value;
5827            self.consume(TokenType::Colon)?;
5828            match field.as_str() {
5829                "days" => {
5830                    span.day_start = self.consume_any_ident_or_kw()?.value;
5831                    self.consume(TokenType::DotDot)?;
5832                    span.day_end = self.consume_any_ident_or_kw()?.value;
5833                }
5834                "hours" => {
5835                    span.hour_start = self.consume_number()? as i64;
5836                    self.consume(TokenType::DotDot)?;
5837                    span.hour_end = self.consume_number()? as i64;
5838                }
5839                _ => self.skip_value(),
5840            }
5841            if self.check(TokenType::Comma) {
5842                self.advance();
5843            }
5844        }
5845        self.consume(TokenType::RBrace)?;
5846        Ok(span)
5847    }
5848
5849    fn parse_shield(&mut self) -> Result<ShieldDefinition, ParseError> {
5850        let tok = self.consume(TokenType::Shield)?;
5851        let name = self.consume(TokenType::Identifier)?.value;
5852        let mut node = ShieldDefinition {
5853            name,
5854            scan: Vec::new(),
5855            strategy: String::new(),
5856            on_breach: String::new(),
5857            severity: String::new(),
5858            quarantine: String::new(),
5859            max_retries: None,
5860            confidence_threshold: None,
5861            allow_tools: Vec::new(),
5862            deny_tools: Vec::new(),
5863            sandbox: None,
5864            redact: Vec::new(),
5865            log: String::new(),
5866            deflect_message: String::new(),
5867            taint: String::new(),
5868            compliance: Vec::new(),
5869            sign: String::new(),
5870            unknown_fields: Vec::new(),
5871            loc: Loc {
5872                line: tok.line,
5873                column: tok.column,
5874            },
5875            leading_trivia: Vec::new(),
5876            trailing_trivia: Vec::new(),
5877        };
5878        self.consume(TokenType::LBrace)?;
5879        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5880            let field_name = self.current().value.clone();
5881            let field_loc = Loc {
5882                line: self.current().line,
5883                column: self.current().column,
5884            };
5885            self.advance();
5886            if self.check(TokenType::Colon) {
5887                self.advance();
5888                match field_name.as_str() {
5889                    "scan" => node.scan = self.parse_bracketed_identifiers()?,
5890                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
5891                    "on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
5892                    "severity" => node.severity = self.consume_any_ident_or_kw()?.value.clone(),
5893                    "quarantine" => {
5894                        node.quarantine = self.consume(TokenType::StringLit)?.value.clone()
5895                    }
5896                    "max_retries" => node.max_retries = self.parse_optional_int(),
5897                    "confidence_threshold" => {
5898                        node.confidence_threshold = self.parse_optional_float()
5899                    }
5900                    "allow_tools" => node.allow_tools = self.parse_bracketed_identifiers()?,
5901                    "deny_tools" => node.deny_tools = self.parse_bracketed_identifiers()?,
5902                    "sandbox" => {
5903                        node.sandbox = Some(self.consume_any_ident_or_kw()?.value == "true")
5904                    }
5905                    "redact" => node.redact = self.parse_bracketed_identifiers()?,
5906                    "log" => node.log = self.consume_any_ident_or_kw()?.value.clone(),
5907                    "deflect_message" => {
5908                        node.deflect_message = self.consume(TokenType::StringLit)?.value.clone()
5909                    }
5910                    "taint" => node.taint = self.consume_any_ident_or_kw()?.value.clone(),
5911                    // ESK Fase 6.1 — covered regulatory classes.
5912                    "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
5913                    // §Fase 77.a — egress signing algorithm (closed catalog,
5914                    // validated by the checker: `axon-T846`).
5915                    "sign" => node.sign = self.consume_any_ident_or_kw()?.value.clone(),
5916                    // §Fase 77.a — the value is still skipped (leniency
5917                    // preserved) but the NAME is recorded so the checker
5918                    // emits `axon-W010` instead of a silent drop.
5919                    _ => {
5920                        node.unknown_fields.push((field_name.clone(), field_loc));
5921                        self.skip_value()
5922                    }
5923                }
5924            } else if self.check(TokenType::LBrace) {
5925                self.skip_braced_block()?;
5926            }
5927        }
5928        self.consume(TokenType::RBrace)?;
5929        Ok(node)
5930    }
5931
5932    fn parse_pix(&mut self) -> Result<PixDefinition, ParseError> {
5933        let tok = self.consume(TokenType::Pix)?;
5934        let name = self.consume(TokenType::Identifier)?.value;
5935        let mut node = PixDefinition {
5936            name,
5937            source: String::new(),
5938            depth: None,
5939            branching: None,
5940            model: String::new(),
5941            loc: Loc {
5942                line: tok.line,
5943                column: tok.column,
5944            },
5945            leading_trivia: Vec::new(),
5946            trailing_trivia: Vec::new(),
5947        };
5948        self.consume(TokenType::LBrace)?;
5949        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5950            let field_name = self.current().value.clone();
5951            self.advance();
5952            if self.check(TokenType::Colon) {
5953                self.advance();
5954                match field_name.as_str() {
5955                    "source" => node.source = self.consume(TokenType::StringLit)?.value.clone(),
5956                    "depth" => node.depth = self.parse_optional_int(),
5957                    "branching" => node.branching = self.parse_optional_int(),
5958                    "model" => node.model = self.consume_any_ident_or_kw()?.value.clone(),
5959                    _ => self.skip_value(),
5960                }
5961            } else if self.check(TokenType::LBrace) {
5962                self.skip_braced_block()?;
5963            }
5964        }
5965        self.consume(TokenType::RBrace)?;
5966        Ok(node)
5967    }
5968
5969    /// §Fase 62.0 — `ledger <Name> { source, depth, branching, model }`.
5970    /// The append-only audit chain (formerly the Provenance-Index reading of
5971    /// `pix`). Field grammar mirrors `pix` (same shape) but the SEMANTICS are
5972    /// audit, not navigation: `depth` = chain retention, `branching` = Merkle
5973    /// factor, `model` = hash slug (sha256 / blake3 / sha3).
5974    fn parse_ledger(&mut self) -> Result<LedgerDefinition, ParseError> {
5975        let tok = self.consume(TokenType::Ledger)?;
5976        let name = self.consume(TokenType::Identifier)?.value;
5977        let mut node = LedgerDefinition {
5978            name,
5979            source: String::new(),
5980            depth: None,
5981            branching: None,
5982            model: String::new(),
5983            loc: Loc {
5984                line: tok.line,
5985                column: tok.column,
5986            },
5987            leading_trivia: Vec::new(),
5988            trailing_trivia: Vec::new(),
5989        };
5990        self.consume(TokenType::LBrace)?;
5991        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5992            let field_name = self.current().value.clone();
5993            self.advance();
5994            if self.check(TokenType::Colon) {
5995                self.advance();
5996                match field_name.as_str() {
5997                    "source" => node.source = self.consume(TokenType::StringLit)?.value.clone(),
5998                    "depth" => node.depth = self.parse_optional_int(),
5999                    "branching" => node.branching = self.parse_optional_int(),
6000                    "model" => node.model = self.consume_any_ident_or_kw()?.value.clone(),
6001                    _ => self.skip_value(),
6002                }
6003            } else if self.check(TokenType::LBrace) {
6004                self.skip_braced_block()?;
6005            }
6006        }
6007        self.consume(TokenType::RBrace)?;
6008        Ok(node)
6009    }
6010
6011    fn parse_psyche(&mut self) -> Result<PsycheDefinition, ParseError> {
6012        let tok = self.consume(TokenType::Psyche)?;
6013        let name = self.consume(TokenType::Identifier)?.value;
6014        let mut node = PsycheDefinition {
6015            name,
6016            dimensions: Vec::new(),
6017            manifold_noise: None,
6018            manifold_momentum: None,
6019            safety_constraints: Vec::new(),
6020            quantum_enabled: None,
6021            inference_mode: String::new(),
6022            loc: Loc {
6023                line: tok.line,
6024                column: tok.column,
6025            },
6026            leading_trivia: Vec::new(),
6027            trailing_trivia: Vec::new(),
6028        };
6029        self.consume(TokenType::LBrace)?;
6030        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6031            let field_name = self.current().value.clone();
6032            self.advance();
6033            if self.check(TokenType::Colon) {
6034                self.advance();
6035                match field_name.as_str() {
6036                    "dimensions" => node.dimensions = self.parse_bracketed_identifiers()?,
6037                    "manifold_noise" => node.manifold_noise = self.parse_optional_float(),
6038                    "manifold_momentum" => node.manifold_momentum = self.parse_optional_float(),
6039                    "safety_constraints" => {
6040                        node.safety_constraints = self.parse_bracketed_identifiers()?
6041                    }
6042                    "quantum_enabled" => {
6043                        node.quantum_enabled = Some(self.consume_any_ident_or_kw()?.value == "true")
6044                    }
6045                    "inference_mode" => {
6046                        node.inference_mode = self.consume_any_ident_or_kw()?.value.clone()
6047                    }
6048                    _ => self.skip_value(),
6049                }
6050            } else if self.check(TokenType::LBrace) {
6051                self.skip_braced_block()?;
6052            }
6053        }
6054        self.consume(TokenType::RBrace)?;
6055        Ok(node)
6056    }
6057
6058    fn parse_corpus(&mut self) -> Result<CorpusDefinition, ParseError> {
6059        let tok = self.consume(TokenType::Corpus)?;
6060        let name = self.consume(TokenType::Identifier)?.value;
6061        let mut node = CorpusDefinition {
6062            name,
6063            documents: Vec::new(),
6064            relations: Vec::new(),
6065            adaptive: false,
6066            mcp_server: String::new(),
6067            mcp_resource_uri: String::new(),
6068            store_source: None,
6069            loc: Loc {
6070                line: tok.line,
6071                column: tok.column,
6072            },
6073            leading_trivia: Vec::new(),
6074            trailing_trivia: Vec::new(),
6075        };
6076        // corpus Name from mcp("server", "uri")  — static MCP-bound short form.
6077        // corpus Name from axonstore { documents: S(id,title)  relations: … }  —
6078        // §Fase 64.A dynamic store-sourced MDN graph (falls through to the body).
6079        let mut dynamic = false;
6080        if self.check(TokenType::From) {
6081            self.advance();
6082            if self.check(TokenType::AxonStore) {
6083                self.advance();
6084                dynamic = true;
6085            } else {
6086                self.consume(TokenType::Mcp)?;
6087                self.consume(TokenType::LParen)?;
6088                node.mcp_server = self.consume(TokenType::StringLit)?.value.clone();
6089                self.consume(TokenType::Comma)?;
6090                node.mcp_resource_uri = self.consume(TokenType::StringLit)?.value.clone();
6091                self.consume(TokenType::RParen)?;
6092                return Ok(node);
6093            }
6094        }
6095        self.consume(TokenType::LBrace)?;
6096        // §Fase 64.A — accumulate the store-mapping pieces while the dynamic body
6097        // is parsed; folded into `node.store_source` after the closing brace.
6098        let mut src = CorpusStoreSource {
6099            doc_store: String::new(),
6100            doc_id_col: String::new(),
6101            doc_title_col: String::new(),
6102            edge_store: String::new(),
6103            edge_from_col: String::new(),
6104            edge_to_col: String::new(),
6105            edge_type_col: String::new(),
6106            edge_weight_col: String::new(),
6107            loc: Loc {
6108                line: tok.line,
6109                column: tok.column,
6110            },
6111        };
6112        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6113            let field_name = self.current().value.clone();
6114            self.advance();
6115            if self.check(TokenType::Colon) {
6116                self.advance();
6117                match field_name.as_str() {
6118                    // §Fase 64.A — dynamic: `documents: <DocStore>(id_col, title_col)`.
6119                    "documents" if dynamic => {
6120                        let (store, cols) = self.parse_corpus_store_mapping(2)?;
6121                        src.doc_store = store;
6122                        src.doc_id_col = cols[0].clone();
6123                        src.doc_title_col = cols[1].clone();
6124                    }
6125                    "documents" => node.documents = self.parse_bracketed_identifiers()?,
6126                    // §Fase 64.A — dynamic: `relations: <EdgeStore>(from, to, etype, weight)`.
6127                    "relations" if dynamic => {
6128                        let (store, cols) = self.parse_corpus_store_mapping(4)?;
6129                        src.edge_store = store;
6130                        src.edge_from_col = cols[0].clone();
6131                        src.edge_to_col = cols[1].clone();
6132                        src.edge_type_col = cols[2].clone();
6133                        src.edge_weight_col = cols[3].clone();
6134                    }
6135                    // §Fase 63.A — static typed weighted edges → MDN corpus graph.
6136                    "relations" => node.relations = self.parse_corpus_relations()?,
6137                    // §Fase 63.C — enable the memory endofunctor.
6138                    "adaptive" => node.adaptive = self.consume_any_ident_or_kw()?.value == "true",
6139                    _ => self.skip_value(),
6140                }
6141            } else if self.check(TokenType::LBrace) {
6142                self.skip_braced_block()?;
6143            }
6144        }
6145        self.consume(TokenType::RBrace)?;
6146        if dynamic {
6147            node.store_source = Some(src);
6148        }
6149        Ok(node)
6150    }
6151
6152    /// §Fase 64.A — parse a store-mapping `<StoreName>( col1, col2, … )` of exactly
6153    /// `n` columns. Used by the dynamic store-sourced corpus's `documents:` (2
6154    /// cols: id, title) and `relations:` (4 cols: from, to, etype, weight). The
6155    /// store name is an identifier (a declared `axonstore`); the columns may be
6156    /// keywords (a column could be named `from`/`type`), so they use the
6157    /// keyword-tolerant consumer. The type-checker validates store + columns.
6158    fn parse_corpus_store_mapping(&mut self, n: usize) -> Result<(String, Vec<String>), ParseError> {
6159        let store = self.consume(TokenType::Identifier)?.value.clone();
6160        self.consume(TokenType::LParen)?;
6161        let mut cols = Vec::with_capacity(n);
6162        for i in 0..n {
6163            if i > 0 {
6164                self.consume(TokenType::Comma)?;
6165            }
6166            cols.push(self.consume_any_ident_or_kw()?.value.clone());
6167        }
6168        self.consume(TokenType::RParen)?;
6169        Ok((store, cols))
6170    }
6171
6172    /// §Fase 63.A — parse `relations: [ etype(from, to, weight) … ]`, the typed
6173    /// weighted edges of an MDN corpus graph. Entries are whitespace/newline
6174    /// separated; commas between them are optional. Edge-type validity (closed
6175    /// catalog), document references, and the weight range are checked by the
6176    /// type-checker (`check_corpus`), not here.
6177    fn parse_corpus_relations(&mut self) -> Result<Vec<CorpusRelation>, ParseError> {
6178        let mut out = Vec::new();
6179        self.consume(TokenType::LBracket)?;
6180        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
6181            if self.check(TokenType::Comma) {
6182                self.advance();
6183                continue;
6184            }
6185            let tok = self.current().clone();
6186            let etype = self.consume_any_ident_or_kw()?.value.clone();
6187            self.consume(TokenType::LParen)?;
6188            let from = self.consume_any_ident_or_kw()?.value.clone();
6189            self.consume(TokenType::Comma)?;
6190            let to = self.consume_any_ident_or_kw()?.value.clone();
6191            self.consume(TokenType::Comma)?;
6192            let weight = self.consume_number()?;
6193            self.consume(TokenType::RParen)?;
6194            out.push(CorpusRelation {
6195                etype,
6196                from,
6197                to,
6198                weight,
6199                loc: Loc { line: tok.line, column: tok.column },
6200            });
6201        }
6202        self.consume(TokenType::RBracket)?;
6203        Ok(out)
6204    }
6205
6206    /// §Fase 108.b — the typed dataspace declaration:
6207    ///
6208    /// ```text
6209    /// dataspace <Name> {
6210    ///     column <name>: <Type>
6211    ///     …
6212    /// }
6213    /// ```
6214    ///
6215    /// Until 108.b the body was consumed by `skip_braced_block()` — any
6216    /// content, including garbage, compiled clean and reached nothing.
6217    /// Now each entry must be a `column` field; the declared type is
6218    /// kept RAW here and resolved against the closed 6-type catalog by
6219    /// the type-checker (`axon-T928`), so all schema errors accumulate
6220    /// in a single compile. An unknown body keyword is a parse error
6221    /// (the grammar is closed — the §38 axonstore posture).
6222    fn parse_dataspace(&mut self) -> Result<DataspaceDefinition, ParseError> {
6223        let tok = self.consume(TokenType::Dataspace)?;
6224        let name = self.consume(TokenType::Identifier)?.value;
6225        let mut node = DataspaceDefinition {
6226            name,
6227            columns: Vec::new(),
6228            loc: Loc {
6229                line: tok.line,
6230                column: tok.column,
6231            },
6232            leading_trivia: Vec::new(),
6233            trailing_trivia: Vec::new(),
6234        };
6235        if self.check(TokenType::LBrace) {
6236            self.consume(TokenType::LBrace)?;
6237            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6238                let entry = self.current().clone();
6239                if entry.value != "column" {
6240                    return Err(ParseError {
6241                        message: format!(
6242                            "Unknown entry `{}` in dataspace `{}`. A dataspace body \
6243                             declares its columnar schema: `column <name>: <Type>` \
6244                             (one per line, over the closed type catalog — \
6245                             Text, Int, Float, Bool, Timestamp, Json).",
6246                            entry.value, node.name
6247                        ),
6248                        line: entry.line,
6249                        column: entry.column,
6250                        ..Default::default()
6251                    });
6252                }
6253                self.advance(); // `column`
6254                let col_tok = self.current().clone();
6255                let col_name = self.consume_any_ident_or_kw()?.value.clone();
6256                self.consume(TokenType::Colon)?;
6257                let declared_type = self.consume_any_ident_or_kw()?.value.clone();
6258                node.columns.push(crate::ast::DataspaceColumn {
6259                    name: col_name,
6260                    declared_type,
6261                    loc: Loc {
6262                        line: col_tok.line,
6263                        column: col_tok.column,
6264                    },
6265                });
6266            }
6267            self.consume(TokenType::RBrace)?;
6268        }
6269        Ok(node)
6270    }
6271
6272    fn parse_ots(&mut self) -> Result<OtsDefinition, ParseError> {
6273        let tok = self.consume(TokenType::Ots)?;
6274        let name = self.consume(TokenType::Identifier)?.value;
6275        let mut node = OtsDefinition {
6276            name,
6277            teleology: String::new(),
6278            homotopy_search: String::new(),
6279            loss_function: String::new(),
6280            loc: Loc {
6281                line: tok.line,
6282                column: tok.column,
6283            },
6284            leading_trivia: Vec::new(),
6285            trailing_trivia: Vec::new(),
6286        };
6287        // Skip optional type params <In, Out>
6288        if self.check(TokenType::Lt) {
6289            while !self.check(TokenType::Gt) && !self.check(TokenType::Eof) {
6290                self.advance();
6291            }
6292            if self.check(TokenType::Gt) {
6293                self.advance();
6294            }
6295        }
6296        self.consume(TokenType::LBrace)?;
6297        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6298            let field_name = self.current().value.clone();
6299            self.advance();
6300            if self.check(TokenType::Colon) {
6301                self.advance();
6302                match field_name.as_str() {
6303                    "teleology" => {
6304                        node.teleology = self.consume(TokenType::StringLit)?.value.clone()
6305                    }
6306                    "homotopy_search" => {
6307                        node.homotopy_search = self.consume_any_ident_or_kw()?.value.clone()
6308                    }
6309                    "loss_function" => {
6310                        node.loss_function = self.consume(TokenType::StringLit)?.value.clone()
6311                    }
6312                    _ => self.skip_value(),
6313                }
6314            } else if self.check(TokenType::LBrace) {
6315                self.skip_braced_block()?;
6316            }
6317        }
6318        self.consume(TokenType::RBrace)?;
6319        Ok(node)
6320    }
6321
6322    fn parse_mandate(&mut self) -> Result<MandateDefinition, ParseError> {
6323        let tok = self.consume(TokenType::Mandate)?;
6324        let name = self.consume(TokenType::Identifier)?.value;
6325        let mut node = MandateDefinition {
6326            name,
6327            constraint: String::new(),
6328            kp: None,
6329            ki: None,
6330            kd: None,
6331            tolerance: None,
6332            max_steps: None,
6333            on_violation: String::new(),
6334            loc: Loc {
6335                line: tok.line,
6336                column: tok.column,
6337            },
6338            leading_trivia: Vec::new(),
6339            trailing_trivia: Vec::new(),
6340        };
6341        self.consume(TokenType::LBrace)?;
6342        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6343            let field_name = self.current().value.clone();
6344            self.advance();
6345            if self.check(TokenType::Colon) {
6346                self.advance();
6347                match field_name.as_str() {
6348                    "constraint" => {
6349                        node.constraint = self.consume(TokenType::StringLit)?.value.clone()
6350                    }
6351                    "kp" | "Kp" => node.kp = self.parse_optional_float(),
6352                    "ki" | "Ki" => node.ki = self.parse_optional_float(),
6353                    "kd" | "Kd" => node.kd = self.parse_optional_float(),
6354                    "tolerance" => node.tolerance = self.parse_optional_float(),
6355                    "max_steps" => node.max_steps = self.parse_optional_int(),
6356                    "on_violation" => {
6357                        node.on_violation = self.consume_any_ident_or_kw()?.value.clone()
6358                    }
6359                    _ => self.skip_value(),
6360                }
6361            } else if self.check(TokenType::LBrace) {
6362                self.skip_braced_block()?;
6363            }
6364        }
6365        self.consume(TokenType::RBrace)?;
6366        Ok(node)
6367    }
6368
6369    /// §Fase 111.f — `compute <Name>(p: T, …) -> T { <expr> }`.
6370    ///
6371    /// # What this used to be
6372    ///
6373    /// ```text
6374    /// // Skip optional parameters/return type before brace
6375    /// while !self.check(TokenType::LBrace) { self.advance(); }
6376    /// ```
6377    ///
6378    /// The parameters and the return type were **skipped token by token**, and
6379    /// the brace held only `shield:`. So a `compute` had **no inputs, no output
6380    /// type and no body** — which is why the runtime could do nothing but bind
6381    /// the literal string `"compute:Name(args)"`, and why a downstream step then
6382    /// consumed that text where it expected a number. The README meanwhile
6383    /// promised "native Fast-Path execution bypassing the LLM" **with an O(n)
6384    /// guarantee**.
6385    ///
6386    /// # What it is now
6387    ///
6388    /// A named pure function over the §70 expression language — the closed,
6389    /// total, side-effect-free term algebra the runtime already evaluates
6390    /// natively (`eval_expr`, the same evaluator behind `let`, `grad` and
6391    /// `conditional`). Linear in the term, no model in the loop: the advertised
6392    /// claim, made true rather than louder.
6393    ///
6394    /// The legacy field form (`compute N { shield: G }`) still parses — its body
6395    /// is simply `None`, and applying a bodyless compute is refused (axon-T941)
6396    /// instead of silently binding a placeholder.
6397    fn parse_compute(&mut self) -> Result<ComputeDefinition, ParseError> {
6398        let tok = self.consume(TokenType::Compute)?;
6399        let name = self.consume(TokenType::Identifier)?.value;
6400        let mut node = ComputeDefinition {
6401            name,
6402            shield_ref: String::new(),
6403            parameters: Vec::new(),
6404            return_type: String::new(),
6405            body: None,
6406            loc: Loc {
6407                line: tok.line,
6408                column: tok.column,
6409            },
6410            leading_trivia: Vec::new(),
6411            trailing_trivia: Vec::new(),
6412        };
6413
6414        // `(p: T, q: T)` — the typed parameters (they used to be skipped).
6415        if self.check(TokenType::LParen) {
6416            self.advance();
6417            while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
6418                let ptok = self.current().clone();
6419                let pname = self.consume_any_ident_or_kw()?.value.clone();
6420                self.consume(TokenType::Colon)?;
6421                let ptype = self.parse_type_expr()?;
6422                node.parameters.push(Parameter {
6423                    name: pname,
6424                    type_expr: ptype,
6425                    loc: self.loc_of(&ptok),
6426                });
6427                if self.check(TokenType::Comma) {
6428                    self.advance();
6429                }
6430            }
6431            self.consume(TokenType::RParen)?;
6432        }
6433
6434        // `-> T` — the declared result type.
6435        if self.check(TokenType::Arrow) {
6436            self.advance();
6437            node.return_type = self.consume_any_ident_or_kw()?.value.clone();
6438        }
6439
6440        self.consume(TokenType::LBrace)?;
6441        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6442            // A `<name>:` pair is a legacy field (only `shield:` is meaningful).
6443            // Anything else is THE BODY — a §70 expression.
6444            //
6445            // NOTE: the field name may be a KEYWORD, not just an identifier —
6446            // `shield` is `TokenType::Shield`. Testing only for `Identifier` here
6447            // sent `compute N { shield: G }` (the legacy declaration form, and
6448            // the shape of the shipped canonical program) down the
6449            // expression-parsing path and broke it. Back-compat is not optional:
6450            // an adopter's existing program must keep compiling.
6451            let is_field = self
6452                .tokens
6453                .get(self.pos + 1)
6454                .map(|t| t.ttype == TokenType::Colon)
6455                .unwrap_or(false);
6456            if is_field {
6457                let field_name = self.current().value.clone();
6458                self.advance();
6459                self.consume(TokenType::Colon)?;
6460                match field_name.as_str() {
6461                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
6462                    _ => self.skip_value(),
6463                }
6464            } else {
6465                node.body = Some(self.parse_expr()?);
6466            }
6467        }
6468        self.consume(TokenType::RBrace)?;
6469        Ok(node)
6470    }
6471
6472    fn parse_daemon(&mut self) -> Result<DaemonDefinition, ParseError> {
6473        let tok = self.consume(TokenType::Daemon)?;
6474        let name = self.consume(TokenType::Identifier)?.value;
6475        let mut node = DaemonDefinition {
6476            name,
6477            goal: String::new(),
6478            tools: Vec::new(),
6479            memory_ref: String::new(),
6480            strategy: String::new(),
6481            on_stuck: String::new(),
6482            shield_ref: String::new(),
6483            window_ref: String::new(),
6484            budget: None,
6485            max_tokens: None,
6486            max_time: String::new(),
6487            max_cost: None,
6488            listeners: Vec::new(),
6489            requires_capabilities: Vec::new(),
6490            loc: Loc {
6491                line: tok.line,
6492                column: tok.column,
6493            },
6494            leading_trivia: Vec::new(),
6495            trailing_trivia: Vec::new(),
6496        };
6497        // Skip optional parameters/return type before brace
6498        while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
6499            self.advance();
6500        }
6501        self.consume(TokenType::LBrace)?;
6502        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6503            let field = self.current().clone();
6504            let field_name = field.value.clone();
6505            self.advance();
6506            if self.check(TokenType::Colon) {
6507                self.advance();
6508                match field_name.as_str() {
6509                    "goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
6510                    "tools" => node.tools = self.parse_bracketed_identifiers()?,
6511                    "memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
6512                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
6513                    "on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
6514                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
6515                    // §Fase 71.c — `window: <WindowName>` temporal binding.
6516                    "window" => node.window_ref = self.consume_any_ident_or_kw()?.value.clone(),
6517                    "max_tokens" => node.max_tokens = self.parse_optional_int(),
6518                    "max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
6519                    "max_cost" => node.max_cost = self.parse_optional_float(),
6520                    // §Fase 52.d — `requires: [cap, …]` capability scope (same
6521                    // closed slug grammar as `axonendpoint requires:`). The
6522                    // enterprise supervisor mints a per-run principal scoped to
6523                    // exactly these (least privilege).
6524                    "requires" => {
6525                        let bracket_tok = self.current().clone();
6526                        let items = self.parse_bracketed_dot_identifiers()?;
6527                        for slug in &items {
6528                            if !is_valid_capability_slug(slug) {
6529                                return Err(ParseError {
6530                                    message: format!(
6531                                        "Invalid capability slug '{slug}' in daemon '{}' \
6532                                         `requires:`. Capability slugs must match \
6533                                         ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
6534                                         lowercase identifiers. Examples: `daemon.run`, \
6535                                         `memory.write`, `flow.execute`.",
6536                                        node.name
6537                                    ),
6538                                    line: bracket_tok.line,
6539                                    column: bracket_tok.column,
6540                                    ..Default::default()
6541                                });
6542                            }
6543                        }
6544                        node.requires_capabilities = items;
6545                    }
6546                    _ => self.skip_value(),
6547                }
6548            } else if field.ttype == TokenType::Listen {
6549                // §λ-L-E Fase 13 D4 — preserve listen blocks for type
6550                // checking.  We backtracked past the `listen` keyword
6551                // by `advance()` above, so reconstruct a synthetic
6552                // listener using the same dual-mode dispatch the flow
6553                // step parser uses (string topic OR typed channel ref).
6554                let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
6555                    (self.consume(TokenType::StringLit)?.value.clone(), false)
6556                } else {
6557                    (self.consume_any_ident_or_kw()?.value.clone(), true)
6558                };
6559                let mut alias = String::new();
6560                if !self.at_declaration_start()
6561                    && !self.check(TokenType::RBrace)
6562                    && !self.check(TokenType::LBrace)
6563                {
6564                    let next = self.current().clone();
6565                    if next.value == "as" || next.ttype == TokenType::As {
6566                        self.advance();
6567                        alias = self.consume_any_ident_or_kw()?.value.clone();
6568                    }
6569                }
6570                let listen_loc = Loc {
6571                    line: field.line,
6572                    column: field.column,
6573                };
6574                // §Fase 52.a — parse the handler body (was skipped). This is
6575                // what makes a `daemon` operational: the body runs per event /
6576                // scheduled tick (e.g. a `listen "cron:…" as tick { run … }`).
6577                let body = self.parse_listener_body()?;
6578                node.listeners.push(ListenStep {
6579                    channel,
6580                    channel_is_ref,
6581                    event_alias: alias,
6582                    body,
6583                    loc: listen_loc,
6584                });
6585            } else if field_name == "budget" && self.check(TokenType::LBrace) {
6586                // §Fase 72.a — the `budget { … }` linear-effect rate-limit block.
6587                node.budget = Some(self.parse_budget_block(field.line, field.column)?);
6588            } else if self.check(TokenType::LBrace) {
6589                self.skip_braced_block()?;
6590            }
6591        }
6592        self.consume(TokenType::RBrace)?;
6593        Ok(node)
6594    }
6595
6596    /// §Fase 114.a — a TOP-LEVEL `budget <Name> { … }`.
6597    ///
6598    /// Same body as the daemon-attached block; what it gains is a **name** and a
6599    /// **scope that is not a daemon**. Until §114, `budget` was a field of `daemon`
6600    /// and of nothing else — so an adopter deploying an HTTP endpoint that calls a
6601    /// vendor tool had **no way in the language to bound how often it did that.**
6602    /// Not "the bound did not work": **the bound could not be written.** And the
6603    /// HTTP endpoint is what people actually deploy.
6604    fn parse_top_level_budget(&mut self) -> Result<BudgetBlock, ParseError> {
6605        let kw = self.consume(TokenType::Budget)?; // `budget`
6606        let name = self.consume(TokenType::Identifier)?.value;
6607        let mut block = self.parse_budget_block(kw.line, kw.column)?;
6608        block.name = name;
6609        Ok(block)
6610    }
6611
6612    /// §Fase 72.a — `budget { <rate|max>: N per <period> on Tool(<X>) … [on_exhausted: <p>] }`.
6613    fn parse_budget_block(&mut self, line: u32, column: u32) -> Result<BudgetBlock, ParseError> {
6614        self.consume(TokenType::LBrace)?;
6615        let mut quotas = Vec::new();
6616        let mut on_exhausted = String::new();
6617        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6618            let field = self.current().clone();
6619            let field_name = self.consume_any_ident_or_kw()?.value;
6620            match field_name.as_str() {
6621                "rate" | "max" => {
6622                    quotas.push(self.parse_budget_quota(field_name, field.line, field.column)?);
6623                }
6624                "on_exhausted" => {
6625                    self.consume(TokenType::Colon)?;
6626                    on_exhausted = self.consume_any_ident_or_kw()?.value;
6627                }
6628                _ => self.skip_value(),
6629            }
6630        }
6631        self.consume(TokenType::RBrace)?;
6632        Ok(BudgetBlock {
6633            name: String::new(),
6634            quotas,
6635            on_exhausted,
6636            loc: Loc { line, column },
6637            leading_trivia: Vec::new(),
6638            trailing_trivia: Vec::new(),
6639        })
6640    }
6641
6642    /// §Fase 72.a — one quota line: `<kind>: <limit> per <period> on Tool(<effect>)`.
6643    /// `kind` (`rate`/`max`) is already consumed by the caller.
6644    fn parse_budget_quota(
6645        &mut self,
6646        kind: String,
6647        line: u32,
6648        column: u32,
6649    ) -> Result<BudgetQuota, ParseError> {
6650        self.consume(TokenType::Colon)?;
6651        let limit = self.consume_number()? as i64;
6652        // `per <period>`
6653        let _per = self.consume_any_ident_or_kw()?; // the `per` keyword
6654        let period = self.consume_any_ident_or_kw()?.value;
6655        // `on Tool(<effect>)`
6656        let _on = self.consume_any_ident_or_kw()?; // the `on` keyword
6657        let _tool = self.consume_any_ident_or_kw()?; // the `Tool` wrapper keyword
6658        self.consume(TokenType::LParen)?;
6659        let effect = self.consume_any_ident_or_kw()?.value;
6660        self.consume(TokenType::RParen)?;
6661        Ok(BudgetQuota {
6662            kind,
6663            limit,
6664            period,
6665            effect,
6666            loc: Loc { line, column },
6667        })
6668    }
6669
6670    fn parse_axonstore(&mut self) -> Result<AxonStoreDefinition, ParseError> {
6671        let tok = self.consume(TokenType::AxonStore)?;
6672        let name = self.consume(TokenType::Identifier)?.value;
6673        let mut node = AxonStoreDefinition {
6674            name,
6675            backend: String::new(),
6676            connection: String::new(),
6677            resource_ref: String::new(),
6678            confidence_floor: None,
6679            isolation: String::new(),
6680            on_breach: String::new(),
6681            capability: String::new(),
6682            class: String::new(),
6683            column_schema: None,
6684            loc: Loc {
6685                line: tok.line,
6686                column: tok.column,
6687            },
6688            leading_trivia: Vec::new(),
6689            trailing_trivia: Vec::new(),
6690        };
6691        self.consume(TokenType::LBrace)?;
6692        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6693            let field = self.current().clone();
6694            let field_name = field.value.clone();
6695            // §Fase 38.b (D1) — `schema:` declaration in three closed
6696            // forms: inline column block, manifest reference (string
6697            // literal), or env-var schema namespace (`env:VAR` —
6698            // unquoted or quoted). Parse the form; the §38.d / §38.e
6699            // type-checker consumes the resulting AST.
6700            if field.ttype == TokenType::Schema {
6701                self.advance();
6702                let parsed = self.parse_store_schema_declaration(&node.name, field.line, field.column)?;
6703                node.column_schema = Some(parsed);
6704                continue;
6705            }
6706            self.advance();
6707            if self.check(TokenType::Colon) {
6708                self.advance();
6709                match field_name.as_str() {
6710                    "backend" => node.backend = self.consume_any_ident_or_kw()?.value.clone(),
6711                    // §Fase 94.a — the secret-class prefix of a
6712                    // `backend: secrets` metadata store. Dotted-identifier
6713                    // form (`class: crm`, `class: crm.oauth`); the
6714                    // secrets-only placement rule + slug shape are
6715                    // `axon-T900` in the type-checker (it needs the
6716                    // resolved `backend:`, which may appear after this
6717                    // field in source order).
6718                    "class" => node.class = self.parse_dotted_identifier()?,
6719                    "connection" => {
6720                        node.connection = self.consume(TokenType::StringLit)?.value.clone()
6721                    }
6722                    // §Fase 113 — the `resource` this store RUNS ON. When
6723                    // present the store derives its DSN, its POOL SIZE and its
6724                    // sharing discipline from the resource; `connection:`
6725                    // becomes redundant and `axon-T946` refuses declaring both
6726                    // (the same fact, twice, is how the islands happened).
6727                    "resource" => {
6728                        node.resource_ref = self.consume_any_ident_or_kw()?.value.clone()
6729                    }
6730                    "confidence_floor" => node.confidence_floor = self.parse_optional_float(),
6731                    "isolation" => node.isolation = self.consume_any_ident_or_kw()?.value.clone(),
6732                    "on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
6733                    // §Fase 35.j (D11) — Pillar IV: the capability slug
6734                    // required to access this store. Validated against
6735                    // the closed slug grammar shared with `requires:`.
6736                    "capability" => {
6737                        let slug_tok = self.consume(TokenType::StringLit)?.clone();
6738                        if !is_valid_capability_slug(&slug_tok.value) {
6739                            return Err(ParseError {
6740                                message: format!(
6741                                    "Invalid capability slug '{}' in axonstore '{}' \
6742                                     `capability:`. Capability slugs must match \
6743                                     ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
6744                                     lowercase identifiers starting with a letter. Examples: \
6745                                     `admin`, `tenant.read`, `hipaa.phi.read`.",
6746                                    slug_tok.value, node.name
6747                                ),
6748                                line: slug_tok.line,
6749                                column: slug_tok.column,
6750                                ..Default::default()
6751                            });
6752                        }
6753                        node.capability = slug_tok.value.clone();
6754                    }
6755                    _ => self.skip_value(),
6756                }
6757            } else if self.check(TokenType::LBrace) {
6758                self.skip_braced_block()?;
6759            }
6760        }
6761        self.consume(TokenType::RBrace)?;
6762        Ok(node)
6763    }
6764
6765    /// §Fase 38.b (D1) — parse the three closed forms of an `axonstore`
6766    /// `schema:` declaration:
6767    ///
6768    ///   * form (a) **inline** — `schema { col: Type [constraint…], … }`
6769    ///   * form (b) **manifest reference** — `schema: "qualified.name"`
6770    ///     (string literal that does NOT start with `env:`)
6771    ///   * form (c) **env-var schema namespace** — `schema: env:VAR`
6772    ///     (unquoted) OR `schema: "env:VAR"` (quoted; the literal
6773    ///     starts with `env:`)
6774    ///
6775    /// Called immediately AFTER `schema` is consumed.
6776    fn parse_store_schema_declaration(
6777        &mut self,
6778        store_name: &str,
6779        sch_line: u32,
6780        sch_col: u32,
6781    ) -> Result<crate::store_schema::StoreColumnSchema, ParseError> {
6782        use crate::store_schema::{StoreColumn, StoreColumnSchema, StoreColumnType};
6783
6784        // — Form (a) — inline column block: `schema { ... }`. —
6785        if self.check(TokenType::LBrace) {
6786            self.consume(TokenType::LBrace)?;
6787            let mut columns: Vec<StoreColumn> = Vec::new();
6788            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6789                let col_tok = self.current().clone();
6790                let col_name = self.consume_any_ident_or_kw()?.value.clone();
6791                self.consume(TokenType::Colon)?;
6792                let type_tok = self.consume_any_ident_or_kw()?.clone();
6793                let col_type = StoreColumnType::from_token(&type_tok.value).ok_or_else(|| {
6794                    let names = StoreColumnType::all_canonical_names();
6795                    let suggestion =
6796                        crate::smart_suggest::suggest_for(&type_tok.value, &names);
6797                    let suggest_suffix = if suggestion.is_empty() {
6798                        String::new()
6799                    } else {
6800                        format!(" {suggestion}")
6801                    };
6802                    let known = names.join(", ");
6803                    ParseError {
6804                        message: format!(
6805                            "Unknown column type `{}` for column `{}` in \
6806                             axonstore `{}` `schema:` block. The closed \
6807                             v1.38.0 column-type catalog (Fase 38.b D1) \
6808                             is {{{known}}} (plus common lowercase \
6809                             aliases — `int`/`integer`/`int4` for \
6810                             `Int`, `bool`/`boolean` for `Bool`, etc.).\
6811                             {suggest_suffix}",
6812                            type_tok.value, col_name, store_name
6813                        ),
6814                        line: type_tok.line,
6815                        column: type_tok.column,
6816                        ..Default::default()
6817                    }
6818                })?;
6819
6820                // §Fase 73.a (D1) — the OPTIONAL `Json<T>` shape LENS on a
6821                // column. `payload: Json<UserEvent>` records the expected
6822                // struct shape; the lens is a compile-time expectation only
6823                // (the column stays physically `jsonb`, navigated totally at
6824                // runtime — doctrine `open_data_is_total`). The shape's
6825                // well-formedness (T is a declared `type`) is `axon-T840`
6826                // in the type-checker — it needs the symbol table. Here we
6827                // only enforce the STRUCTURAL rule: a `<T>` lens may refine
6828                // ONLY a `Json` / `Jsonb` column — `axon-T841` otherwise.
6829                let mut json_shape: Option<String> = None;
6830                if self.check(TokenType::Lt) {
6831                    self.advance();
6832                    let shape_tok = self.consume_any_ident_or_kw()?.clone();
6833                    self.consume(TokenType::Gt)?;
6834                    if matches!(col_type, StoreColumnType::Json | StoreColumnType::Jsonb) {
6835                        json_shape = Some(shape_tok.value.clone());
6836                    } else {
6837                        return Err(ParseError {
6838                            message: format!(
6839                                "axon-T841 a shape lens `<{shape}>` may refine \
6840                                 only a `Json` / `Jsonb` column, but column \
6841                                 `{col}` in axonstore `{store}` is `{ty}`. Drop \
6842                                 the `<{shape}>` (a rigid column already has a \
6843                                 fixed shape), or change the column type to \
6844                                 `Json<{shape}>` if it carries open documents.",
6845                                shape = shape_tok.value,
6846                                col = col_name,
6847                                store = store_name,
6848                                ty = col_type.canonical_name(),
6849                            ),
6850                            line: shape_tok.line,
6851                            column: shape_tok.column,
6852                            ..Default::default()
6853                        });
6854                    }
6855                }
6856
6857                let mut col = StoreColumn {
6858                    name: col_name,
6859                    col_type,
6860                    json_shape,
6861                    primary_key: false,
6862                    auto_increment: false,
6863                    not_null: false,
6864                    unique: false,
6865                    indexed: false,
6866                    default_value: String::new(),
6867                    // §Fase 38.x.d (D1) — `identity` is now a recognized
6868                    // inline keyword (see the constraint loop below).
6869                    // Defaults to false; set to true when the adopter
6870                    // writes `id: BigInt primary_key identity`.
6871                    identity: false,
6872                    line: col_tok.line,
6873                    column: col_tok.column,
6874                };
6875
6876                // Trailing constraints (position-independent), matching
6877                // the Python `_parse_store_column` surface.
6878                while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6879                    if self.current().ttype != TokenType::Identifier {
6880                        // The next column starts with a non-identifier
6881                        // (rare) — stop the constraint scan.
6882                        break;
6883                    }
6884                    let constraint = self.current().value.clone();
6885                    match constraint.as_str() {
6886                        "primary_key" => {
6887                            col.primary_key = true;
6888                            self.advance();
6889                        }
6890                        "auto_increment" => {
6891                            col.auto_increment = true;
6892                            self.advance();
6893                        }
6894                        "not_null" => {
6895                            col.not_null = true;
6896                            self.advance();
6897                        }
6898                        "unique" => {
6899                            col.unique = true;
6900                            self.advance();
6901                        }
6902                        // §Fase 73.f (D1) — the `index` constraint declares
6903                        // an index as a capability-honest effect (visible to
6904                        // the deploy gate, not a silent DBA action). The
6905                        // backend picks the method from the column type
6906                        // (GIN for a Json/Jsonb column, b-tree otherwise).
6907                        "index" => {
6908                            col.indexed = true;
6909                            self.advance();
6910                        }
6911                        // §Fase 38.x.d (D1) — `identity` marks a column
6912                        // as `GENERATED ALWAYS/BY DEFAULT AS IDENTITY`.
6913                        // Distinct from `auto_increment` (legacy SERIAL
6914                        // via `nextval(...)` default). T803 skips
6915                        // identity columns from the NOT-NULL-omission
6916                        // check because Postgres auto-fills them; the
6917                        // distinction matters because IDENTITY ALWAYS
6918                        // also rejects user-supplied values, where
6919                        // SERIAL accepts them (a future 38.x.e arm in
6920                        // T802 may surface this).
6921                        "identity" => {
6922                            col.identity = true;
6923                            self.advance();
6924                        }
6925                        "default" => {
6926                            self.advance();
6927                            let dv = self.current().clone();
6928                            if matches!(
6929                                dv.ttype,
6930                                TokenType::StringLit
6931                                    | TokenType::Integer
6932                                    | TokenType::Float
6933                            ) {
6934                                col.default_value = dv.value.clone();
6935                                self.advance();
6936                            } else {
6937                                col.default_value =
6938                                    self.consume_any_ident_or_kw()?.value.clone();
6939                            }
6940                        }
6941                        _ => break,
6942                    }
6943                }
6944
6945                columns.push(col);
6946            }
6947            self.consume(TokenType::RBrace)?;
6948            return Ok(StoreColumnSchema::Inline {
6949                columns,
6950                leading_trivia: Vec::new(),
6951                line: sch_line,
6952                column: sch_col,
6953            });
6954        }
6955
6956        // — Forms (b) + (c) require a `:` separator. —
6957        if !self.check(TokenType::Colon) {
6958            let cur = self.current().clone();
6959            return Err(ParseError {
6960                message: format!(
6961                    "axonstore `{store_name}` `schema:` declaration expects \
6962                     `{{ … }}` (inline columns), `: \"manifest.ref\"` \
6963                     (manifest reference), or `: env:VAR` (per-tenant schema \
6964                     namespace). Got `{}` instead.",
6965                    cur.value
6966                ),
6967                line: cur.line,
6968                column: cur.column,
6969                ..Default::default()
6970            });
6971        }
6972        self.consume(TokenType::Colon)?;
6973
6974        // — Form (b) or (c)-quoted — string literal value. —
6975        if self.check(TokenType::StringLit) {
6976            let lit = self.consume(TokenType::StringLit)?.clone();
6977            let value = lit.value.clone();
6978            if let Some(var) = value.strip_prefix("env:") {
6979                let var = var.trim();
6980                if var.is_empty() {
6981                    return Err(ParseError {
6982                        message: format!(
6983                            "axonstore `{store_name}` `schema: \"env:\"` is \
6984                             missing the variable name after the `env:` \
6985                             prefix."
6986                        ),
6987                        line: lit.line,
6988                        column: lit.column,
6989                        ..Default::default()
6990                    });
6991                }
6992                return Ok(StoreColumnSchema::EnvVar {
6993                    var_name: var.to_string(),
6994                    line: sch_line,
6995                    column: sch_col,
6996                });
6997            }
6998            // Plain string → manifest reference.
6999            if value.trim().is_empty() {
7000                return Err(ParseError {
7001                    message: format!(
7002                        "axonstore `{store_name}` `schema:` manifest reference \
7003                         is empty. Expected `\"qualified.name\"` — e.g. \
7004                         `\"public.tenants\"`."
7005                    ),
7006                    line: lit.line,
7007                    column: lit.column,
7008                    ..Default::default()
7009                });
7010            }
7011            return Ok(StoreColumnSchema::ManifestRef {
7012                qualified_name: value,
7013                line: sch_line,
7014                column: sch_col,
7015            });
7016        }
7017
7018        // — Form (c) unquoted — `env:VAR`. The lexer emits `env` as an
7019        //   identifier, then `:`, then the identifier var name. —
7020        let env_tok = self.current().clone();
7021        if env_tok.value == "env" {
7022            self.advance();
7023            if !self.check(TokenType::Colon) {
7024                return Err(ParseError {
7025                    message: format!(
7026                        "axonstore `{store_name}` `schema: env` is missing the \
7027                         `:` separator. Expected `schema: env:VAR`."
7028                    ),
7029                    line: env_tok.line,
7030                    column: env_tok.column,
7031                    ..Default::default()
7032                });
7033            }
7034            self.advance(); // past ':'
7035            let var_tok = self.consume_any_ident_or_kw()?.clone();
7036            if var_tok.value.trim().is_empty() {
7037                return Err(ParseError {
7038                    message: format!(
7039                        "axonstore `{store_name}` `schema: env:` is missing \
7040                         the variable name."
7041                    ),
7042                    line: var_tok.line,
7043                    column: var_tok.column,
7044                    ..Default::default()
7045                });
7046            }
7047            return Ok(StoreColumnSchema::EnvVar {
7048                var_name: var_tok.value.clone(),
7049                line: sch_line,
7050                column: sch_col,
7051            });
7052        }
7053
7054        Err(ParseError {
7055            message: format!(
7056                "axonstore `{store_name}` `schema:` declaration expects \
7057                 `{{ … }}` (inline columns), `\"manifest.ref\"` (manifest \
7058                 reference), or `env:VAR` (per-tenant schema namespace). \
7059                 Got `{}` instead.",
7060                env_tok.value
7061            ),
7062            line: env_tok.line,
7063            column: env_tok.column,
7064            ..Default::default()
7065        })
7066    }
7067
7068    // ── §λ-L-E Fase 1 — Resource primitive ────────────────────────
7069
7070    /// Parse: `resource Name { kind, endpoint, capacity, lifetime, certainty_floor, shield }`.
7071    ///
7072    /// Mirrors `axon.compiler.parser.Parser._parse_resource`. Unknown fields
7073    /// are silently skipped (keeps the grammar forward-compatible).
7074    fn parse_resource(&mut self) -> Result<ResourceDefinition, ParseError> {
7075        let tok = self.consume(TokenType::Resource)?;
7076        let name = self.consume(TokenType::Identifier)?.value;
7077        let mut node = ResourceDefinition {
7078            name,
7079            kind: String::new(),
7080            endpoint: String::new(),
7081            capacity: None,
7082            lifetime: "affine".to_string(),
7083            certainty_floor: None,
7084            shield_ref: String::new(),
7085            within: String::new(),
7086            loc: Loc {
7087                line: tok.line,
7088                column: tok.column,
7089            },
7090            leading_trivia: Vec::new(),
7091            trailing_trivia: Vec::new(),
7092        };
7093        self.consume(TokenType::LBrace)?;
7094        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7095            let field_tok = self.current().clone();
7096            let field_name = field_tok.value.clone();
7097            self.advance();
7098            if !self.check(TokenType::Colon) {
7099                // Tolerate stray brace or unknown layout.
7100                if self.check(TokenType::LBrace) {
7101                    self.skip_braced_block()?;
7102                }
7103                continue;
7104            }
7105            self.advance(); // past ':'
7106            match field_name.as_str() {
7107                "kind" => node.kind = self.consume_any_ident_or_kw()?.value,
7108                // §Fase 113 — `endpoint:` accepts BOTH shapes on purpose:
7109                //   - a dotted config key  (`endpoint: db.main`)      — the law
7110                //   - a string literal     (`endpoint: "postgres://…"`) — the sin
7111                //
7112                // The literal is REFUSED, but by `axon-T944`, not by the parser.
7113                // If it died here the adopter would read "Expected StringLit",
7114                // which explains nothing. The law gets to say why: *URLs and
7115                // credentials never appear in source* — the same sentence
7116                // `axon-T850` has been saying to `upstream.resolve` all along.
7117                //
7118                // A diagnostic that names the rule teaches; one that names the
7119                // token type only tells you the compiler is unhappy.
7120                "endpoint" => {
7121                    node.endpoint = if self.check(TokenType::StringLit) {
7122                        self.consume(TokenType::StringLit)?.value
7123                    } else {
7124                        self.parse_dotted_identifier()?
7125                    };
7126                }
7127                "capacity" => {
7128                    node.capacity = self.parse_optional_int();
7129                }
7130                "lifetime" => {
7131                    let lt_tok = self.consume_any_ident_or_kw()?;
7132                    let lt = lt_tok.value;
7133                    if !matches!(lt.as_str(), "linear" | "affine" | "persistent") {
7134                        return Err(ParseError {
7135                            message: format!(
7136                                "Invalid lifetime '{lt}' in resource '{}' — \
7137                                 expected linear | affine | persistent",
7138                                node.name
7139                            ),
7140                            line: lt_tok.line,
7141                            column: lt_tok.column,
7142                                                    ..Default::default()
7143                        });
7144                    }
7145                    node.lifetime = lt;
7146                }
7147                "certainty_floor" => {
7148                    node.certainty_floor = self.parse_optional_float();
7149                }
7150                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
7151                // §Fase 113 — `within: <fabric>`. ONE field, so a resource
7152                // cannot be in two fabrics: Separation-Logic disjointness is
7153                // unrepresentable rather than verified.
7154                "within" => node.within = self.consume_any_ident_or_kw()?.value,
7155                // §Fase 113 — an unknown field is a HARD ERROR, not a shrug.
7156                //
7157                // This arm used to be `_ => self.skip_value()`. That is the same
7158                // family as §111's root cause (`parse_block_step` →
7159                // `skip_braced_block()`, which silently killed four primitives):
7160                // a misspelled `withn:` would have been swallowed without a
7161                // word, and the resource would have governed nothing while
7162                // looking governed. A field the parser does not know is a field
7163                // the adopter believes in and the compiler does not.
7164                unknown => {
7165                    return Err(ParseError {
7166                        message: format!(
7167                            "Unknown field '{unknown}' in resource '{}' — expected one of: \
7168                             kind, endpoint, capacity, lifetime, certainty_floor, shield, within",
7169                            node.name
7170                        ),
7171                        line: field_tok.line,
7172                        column: field_tok.column,
7173                        ..Default::default()
7174                    });
7175                }
7176            }
7177        }
7178        self.consume(TokenType::RBrace)?;
7179        Ok(node)
7180    }
7181
7182    /// Parse: `fabric Name { provider, region, zones, ephemeral, shield }`.
7183    fn parse_fabric(&mut self) -> Result<FabricDefinition, ParseError> {
7184        let tok = self.consume(TokenType::Fabric)?;
7185        let name = self.consume(TokenType::Identifier)?.value;
7186        let mut node = FabricDefinition {
7187            name,
7188            provider: String::new(),
7189            region: String::new(),
7190            zones: None,
7191            ephemeral: None,
7192            shield_ref: String::new(),
7193            loc: Loc {
7194                line: tok.line,
7195                column: tok.column,
7196            },
7197            leading_trivia: Vec::new(),
7198            trailing_trivia: Vec::new(),
7199        };
7200        self.consume(TokenType::LBrace)?;
7201        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7202            let field_name = self.current().value.clone();
7203            self.advance();
7204            if !self.check(TokenType::Colon) {
7205                if self.check(TokenType::LBrace) {
7206                    self.skip_braced_block()?;
7207                }
7208                continue;
7209            }
7210            self.advance(); // past ':'
7211            match field_name.as_str() {
7212                "provider" => node.provider = self.consume_any_ident_or_kw()?.value,
7213                "region" => node.region = self.consume(TokenType::StringLit)?.value,
7214                "zones" => node.zones = self.parse_optional_int(),
7215                "ephemeral" => {
7216                    let b = self.parse_bool()?;
7217                    node.ephemeral = Some(b);
7218                }
7219                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
7220                _ => self.skip_value(),
7221            }
7222        }
7223        self.consume(TokenType::RBrace)?;
7224        Ok(node)
7225    }
7226
7227    /// Parse: `manifest Name { resources, fabric, region, zones, compliance }`.
7228    fn parse_manifest(&mut self) -> Result<ManifestDefinition, ParseError> {
7229        let tok = self.consume(TokenType::Manifest)?;
7230        let name = self.consume(TokenType::Identifier)?.value;
7231        let mut node = ManifestDefinition {
7232            name,
7233            resources: Vec::new(),
7234            fabric_ref: String::new(),
7235            region: String::new(),
7236            zones: None,
7237            compliance: Vec::new(),
7238            loc: Loc {
7239                line: tok.line,
7240                column: tok.column,
7241            },
7242            leading_trivia: Vec::new(),
7243            trailing_trivia: Vec::new(),
7244        };
7245        self.consume(TokenType::LBrace)?;
7246        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7247            let field_name = self.current().value.clone();
7248            self.advance();
7249            if !self.check(TokenType::Colon) {
7250                if self.check(TokenType::LBrace) {
7251                    self.skip_braced_block()?;
7252                }
7253                continue;
7254            }
7255            self.advance();
7256            match field_name.as_str() {
7257                "resources" => node.resources = self.parse_bracketed_identifiers()?,
7258                "fabric" => node.fabric_ref = self.consume_any_ident_or_kw()?.value,
7259                "region" => node.region = self.consume(TokenType::StringLit)?.value,
7260                "zones" => node.zones = self.parse_optional_int(),
7261                "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
7262                _ => self.skip_value(),
7263            }
7264        }
7265        self.consume(TokenType::RBrace)?;
7266        Ok(node)
7267    }
7268
7269    /// Parse: `observe Name from Manifest { sources, quorum, timeout, on_partition, certainty_floor }`.
7270    fn parse_observe(&mut self) -> Result<ObserveDefinition, ParseError> {
7271        let tok = self.consume(TokenType::Observe)?;
7272        let name = self.consume(TokenType::Identifier)?.value;
7273        // `from <Manifest>` — required per Python grammar.
7274        self.consume(TokenType::From)?;
7275        let target = self.consume(TokenType::Identifier)?.value;
7276        let mut node = ObserveDefinition {
7277            name,
7278            target,
7279            sources: Vec::new(),
7280            quorum: None,
7281            timeout: String::new(),
7282            on_partition: "fail".to_string(),
7283            certainty_floor: None,
7284            loc: Loc {
7285                line: tok.line,
7286                column: tok.column,
7287            },
7288            leading_trivia: Vec::new(),
7289            trailing_trivia: Vec::new(),
7290        };
7291        self.consume(TokenType::LBrace)?;
7292        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7293            let field_name = self.current().value.clone();
7294            self.advance();
7295            if !self.check(TokenType::Colon) {
7296                if self.check(TokenType::LBrace) {
7297                    self.skip_braced_block()?;
7298                }
7299                continue;
7300            }
7301            self.advance();
7302            match field_name.as_str() {
7303                "sources" => node.sources = self.parse_bracketed_identifiers()?,
7304                "quorum" => node.quorum = self.parse_optional_int(),
7305                "timeout" => {
7306                    let t = self.current().clone();
7307                    match t.ttype {
7308                        TokenType::Duration | TokenType::StringLit => {
7309                            self.advance();
7310                            node.timeout = t.value;
7311                        }
7312                        _ => node.timeout = self.consume_any_ident_or_kw()?.value,
7313                    }
7314                }
7315                "on_partition" => {
7316                    let p_tok = self.consume_any_ident_or_kw()?;
7317                    let p = p_tok.value;
7318                    if !matches!(p.as_str(), "fail" | "shield_quarantine") {
7319                        return Err(ParseError {
7320                            message: format!(
7321                                "Invalid on_partition '{p}' in observe '{}' — \
7322                                 expected fail | shield_quarantine",
7323                                node.name
7324                            ),
7325                            line: p_tok.line,
7326                            column: p_tok.column,
7327                                                    ..Default::default()
7328                        });
7329                    }
7330                    node.on_partition = p;
7331                }
7332                "certainty_floor" => node.certainty_floor = self.parse_optional_float(),
7333                _ => self.skip_value(),
7334            }
7335        }
7336        self.consume(TokenType::RBrace)?;
7337        Ok(node)
7338    }
7339
7340    // ── §λ-L-E Fase 3 — Control cognitivo ─────────────────────────
7341
7342    /// Parse: `reconcile Name { observe, threshold, tolerance, on_drift, shield, mandate, max_retries }`.
7343    fn parse_reconcile(&mut self) -> Result<ReconcileDefinition, ParseError> {
7344        let tok = self.consume(TokenType::Reconcile)?;
7345        let name = self.consume(TokenType::Identifier)?.value;
7346        let mut node = ReconcileDefinition {
7347            name,
7348            observe_ref: String::new(),
7349            threshold: None,
7350            tolerance: None,
7351            on_drift: "provision".to_string(),
7352            shield_ref: String::new(),
7353            mandate_ref: String::new(),
7354            max_retries: 3,
7355            loc: Loc {
7356                line: tok.line,
7357                column: tok.column,
7358            },
7359            leading_trivia: Vec::new(),
7360            trailing_trivia: Vec::new(),
7361        };
7362        self.consume(TokenType::LBrace)?;
7363        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7364            let field_name = self.current().value.clone();
7365            self.advance();
7366            if !self.check(TokenType::Colon) {
7367                if self.check(TokenType::LBrace) {
7368                    self.skip_braced_block()?;
7369                }
7370                continue;
7371            }
7372            self.advance();
7373            match field_name.as_str() {
7374                "observe" => node.observe_ref = self.consume_any_ident_or_kw()?.value,
7375                "threshold" => node.threshold = self.parse_optional_float(),
7376                "tolerance" => node.tolerance = self.parse_optional_float(),
7377                "on_drift" => {
7378                    let d_tok = self.consume_any_ident_or_kw()?;
7379                    let d = d_tok.value;
7380                    if !matches!(d.as_str(), "provision" | "alert" | "refine") {
7381                        return Err(ParseError {
7382                            message: format!(
7383                                "Invalid on_drift '{d}' in reconcile '{}' — \
7384                                 expected provision | alert | refine",
7385                                node.name
7386                            ),
7387                            line: d_tok.line,
7388                            column: d_tok.column,
7389                                                    ..Default::default()
7390                        });
7391                    }
7392                    node.on_drift = d;
7393                }
7394                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
7395                "mandate" => node.mandate_ref = self.consume_any_ident_or_kw()?.value,
7396                "max_retries" => {
7397                    if let Some(v) = self.parse_optional_int() {
7398                        node.max_retries = v;
7399                    }
7400                }
7401                _ => self.skip_value(),
7402            }
7403        }
7404        self.consume(TokenType::RBrace)?;
7405        Ok(node)
7406    }
7407
7408    /// Parse: `lease Name { resource, duration, acquire, on_expire }`.
7409    fn parse_lease(&mut self) -> Result<LeaseDefinition, ParseError> {
7410        let tok = self.consume(TokenType::Lease)?;
7411        let name = self.consume(TokenType::Identifier)?.value;
7412        let mut node = LeaseDefinition {
7413            name,
7414            resource_ref: String::new(),
7415            duration: String::new(),
7416            acquire: "on_start".to_string(),
7417            on_expire: "anchor_breach".to_string(),
7418            loc: Loc {
7419                line: tok.line,
7420                column: tok.column,
7421            },
7422            leading_trivia: Vec::new(),
7423            trailing_trivia: Vec::new(),
7424        };
7425        self.consume(TokenType::LBrace)?;
7426        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7427            let field_name = self.current().value.clone();
7428            self.advance();
7429            if !self.check(TokenType::Colon) {
7430                if self.check(TokenType::LBrace) {
7431                    self.skip_braced_block()?;
7432                }
7433                continue;
7434            }
7435            self.advance();
7436            match field_name.as_str() {
7437                "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
7438                "duration" => {
7439                    let t = self.current().clone();
7440                    match t.ttype {
7441                        TokenType::Duration | TokenType::StringLit => {
7442                            self.advance();
7443                            node.duration = t.value;
7444                        }
7445                        _ => node.duration = self.consume_any_ident_or_kw()?.value,
7446                    }
7447                }
7448                "acquire" => {
7449                    let a_tok = self.consume_any_ident_or_kw()?;
7450                    let a = a_tok.value;
7451                    if !matches!(a.as_str(), "on_start" | "on_demand") {
7452                        return Err(ParseError {
7453                            message: format!(
7454                                "Invalid acquire '{a}' in lease '{}' — \
7455                                 expected on_start | on_demand",
7456                                node.name
7457                            ),
7458                            line: a_tok.line,
7459                            column: a_tok.column,
7460                                                    ..Default::default()
7461                        });
7462                    }
7463                    node.acquire = a;
7464                }
7465                "on_expire" => {
7466                    let e_tok = self.consume_any_ident_or_kw()?;
7467                    let e = e_tok.value;
7468                    if !matches!(e.as_str(), "anchor_breach" | "release" | "extend") {
7469                        return Err(ParseError {
7470                            message: format!(
7471                                "Invalid on_expire '{e}' in lease '{}' — \
7472                                 expected anchor_breach | release | extend",
7473                                node.name
7474                            ),
7475                            line: e_tok.line,
7476                            column: e_tok.column,
7477                                                    ..Default::default()
7478                        });
7479                    }
7480                    node.on_expire = e;
7481                }
7482                _ => self.skip_value(),
7483            }
7484        }
7485        self.consume(TokenType::RBrace)?;
7486        Ok(node)
7487    }
7488
7489    /// Parse: `ensemble Name { observations, quorum, aggregation, certainty_mode }`.
7490    fn parse_ensemble(&mut self) -> Result<EnsembleDefinition, ParseError> {
7491        let tok = self.consume(TokenType::Ensemble)?;
7492        let name = self.consume(TokenType::Identifier)?.value;
7493        let mut node = EnsembleDefinition {
7494            name,
7495            observations: Vec::new(),
7496            quorum: None,
7497            aggregation: "majority".to_string(),
7498            certainty_mode: "min".to_string(),
7499            loc: Loc {
7500                line: tok.line,
7501                column: tok.column,
7502            },
7503            leading_trivia: Vec::new(),
7504            trailing_trivia: Vec::new(),
7505        };
7506        self.consume(TokenType::LBrace)?;
7507        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7508            let field_name = self.current().value.clone();
7509            self.advance();
7510            if !self.check(TokenType::Colon) {
7511                if self.check(TokenType::LBrace) {
7512                    self.skip_braced_block()?;
7513                }
7514                continue;
7515            }
7516            self.advance();
7517            match field_name.as_str() {
7518                "observations" => node.observations = self.parse_bracketed_identifiers()?,
7519                "quorum" => node.quorum = self.parse_optional_int(),
7520                "aggregation" => {
7521                    let a_tok = self.consume_any_ident_or_kw()?;
7522                    let a = a_tok.value;
7523                    if !matches!(a.as_str(), "majority" | "weighted" | "byzantine") {
7524                        return Err(ParseError {
7525                            message: format!(
7526                                "Invalid aggregation '{a}' in ensemble '{}' — \
7527                                 expected majority | weighted | byzantine",
7528                                node.name
7529                            ),
7530                            line: a_tok.line,
7531                            column: a_tok.column,
7532                                                    ..Default::default()
7533                        });
7534                    }
7535                    node.aggregation = a;
7536                }
7537                "certainty_mode" => {
7538                    let c_tok = self.consume_any_ident_or_kw()?;
7539                    let c = c_tok.value;
7540                    if !matches!(c.as_str(), "min" | "weighted" | "harmonic") {
7541                        return Err(ParseError {
7542                            message: format!(
7543                                "Invalid certainty_mode '{c}' in ensemble '{}' — \
7544                                 expected min | weighted | harmonic",
7545                                node.name
7546                            ),
7547                            line: c_tok.line,
7548                            column: c_tok.column,
7549                                                    ..Default::default()
7550                        });
7551                    }
7552                    node.certainty_mode = c;
7553                }
7554                _ => self.skip_value(),
7555            }
7556        }
7557        self.consume(TokenType::RBrace)?;
7558        Ok(node)
7559    }
7560
7561    // ── §λ-L-E Fase 4 — Topology + π-calculus binary sessions ─────
7562
7563    /// Parse: `session Name { role1: [step, …]  role2: [step, …] }`.
7564    ///
7565    /// The enclosing `parse_session_definition` disambiguates from the session
7566    /// step token `session` (which does not exist) by always entering from the
7567    /// top-level dispatch; the identifier role name is consumed after `{`.
7568    fn parse_session_definition(&mut self) -> Result<SessionDefinition, ParseError> {
7569        let tok = self.consume(TokenType::Session)?;
7570        let name = self.consume(TokenType::Identifier)?.value;
7571        let mut node = SessionDefinition {
7572            name,
7573            roles: Vec::new(),
7574            loc: Loc {
7575                line: tok.line,
7576                column: tok.column,
7577            },
7578            leading_trivia: Vec::new(),
7579            trailing_trivia: Vec::new(),
7580        };
7581        self.consume(TokenType::LBrace)?;
7582        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7583            let role_tok = self.consume_any_ident_or_kw()?;
7584            self.consume(TokenType::Colon)?;
7585            let steps = self.parse_session_steps()?;
7586            node.roles.push(SessionRole {
7587                name: role_tok.value,
7588                steps,
7589                loc: Loc {
7590                    line: role_tok.line,
7591                    column: role_tok.column,
7592                },
7593            });
7594        }
7595        self.consume(TokenType::RBrace)?;
7596        Ok(node)
7597    }
7598
7599    /// §Fase 51.c.2 — Parse a Pauli-sum observable declaration:
7600    /// ```text
7601    /// observable EnergyHamiltonian {
7602    ///     qubits: 2
7603    ///     term: 0.5 * "ZZ"
7604    ///     term: -1.2 * "XI"
7605    /// }
7606    /// ```
7607    /// `term:` is a repeatable key (one `cₖ · Pₖ` per line). The coefficient is
7608    /// a real scalar (optional leading `+`/`-`), then `*`, then a quoted Pauli
7609    /// string. The type-checker (§51.c.2) validates the closed `{I,X,Y,Z}`
7610    /// alphabet + equal lengths; real coefficients ⇒ Hermitian by construction.
7611    fn parse_observable(&mut self) -> Result<ObservableDefinition, ParseError> {
7612        let tok = self.consume(TokenType::Observable)?;
7613        let name = self.consume(TokenType::Identifier)?.value;
7614        let mut node = ObservableDefinition {
7615            name,
7616            qubits: None,
7617            terms: Vec::new(),
7618            loc: Loc {
7619                line: tok.line,
7620                column: tok.column,
7621            },
7622            leading_trivia: Vec::new(),
7623            trailing_trivia: Vec::new(),
7624        };
7625        self.consume(TokenType::LBrace)?;
7626        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7627            let key_tok = self.consume_any_ident_or_kw()?;
7628            self.consume(TokenType::Colon)?;
7629            match key_tok.value.as_str() {
7630                "qubits" => node.qubits = Some(self.consume_number()? as i64),
7631                "term" => {
7632                    let term_loc = Loc {
7633                        line: key_tok.line,
7634                        column: key_tok.column,
7635                    };
7636                    // Optional sign, then magnitude.
7637                    let mut negative = false;
7638                    if self.check(TokenType::Minus) {
7639                        self.advance();
7640                        negative = true;
7641                    } else if self.check(TokenType::Plus) {
7642                        self.advance();
7643                    }
7644                    let mag = self.consume_number()?;
7645                    let coefficient = if negative { -mag } else { mag };
7646                    // `*` separator between coefficient and Pauli string.
7647                    self.consume(TokenType::Star)?;
7648                    let pauli = self.consume(TokenType::StringLit)?.value;
7649                    node.terms.push(PauliTerm {
7650                        coefficient,
7651                        pauli,
7652                        loc: term_loc,
7653                    });
7654                }
7655                _ => self.skip_value(),
7656            }
7657        }
7658        self.consume(TokenType::RBrace)?;
7659        Ok(node)
7660    }
7661
7662    /// §Fase 69.a — Parse:
7663    /// `witness Name { claim: <ref>  against: <baseline>  metric: <metric>
7664    ///                 threshold: <ε>  data: <source> }`.
7665    /// Order-free `key: value` pairs. `claim`/`against`/`metric`/`data` are bare
7666    /// identifiers (a ref or a closed-catalog keyword); `threshold` is a number.
7667    /// Well-formedness (known metric, threshold range, required fields) is the
7668    /// type-checker's job (`axon-E0790`).
7669    fn parse_witness(&mut self) -> Result<WitnessDefinition, ParseError> {
7670        let tok = self.consume(TokenType::Witness)?;
7671        let name = self.consume(TokenType::Identifier)?.value;
7672        let mut node = WitnessDefinition {
7673            name,
7674            claim: String::new(),
7675            baseline: String::new(),
7676            metric: String::new(),
7677            threshold: 0.0,
7678            data: String::new(),
7679            loc: Loc {
7680                line: tok.line,
7681                column: tok.column,
7682            },
7683            leading_trivia: Vec::new(),
7684            trailing_trivia: Vec::new(),
7685        };
7686        self.consume(TokenType::LBrace)?;
7687        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7688            let key_tok = self.consume_any_ident_or_kw()?;
7689            self.consume(TokenType::Colon)?;
7690            match key_tok.value.as_str() {
7691                "claim" => node.claim = self.consume_any_ident_or_kw()?.value,
7692                // `against` is the baseline; `against` is not a reserved keyword,
7693                // so it lexes as an identifier key here.
7694                "against" => node.baseline = self.consume_any_ident_or_kw()?.value,
7695                "metric" => node.metric = self.consume_any_ident_or_kw()?.value,
7696                "threshold" => node.threshold = self.consume_number()?,
7697                "data" => node.data = self.consume_any_ident_or_kw()?.value,
7698                _ => self.skip_value(),
7699            }
7700        }
7701        self.consume(TokenType::RBrace)?;
7702        Ok(node)
7703    }
7704
7705    /// §Fase 41.b — Parse:
7706    /// `socket Name { protocol: SessionRef, backpressure: credit(n),
7707    ///               reconnect: cognitive_state, legal_basis: ... }`.
7708    /// Fields are `key: value` pairs (order-free); only `protocol` is required.
7709    fn parse_socket(&mut self) -> Result<SocketDefinition, ParseError> {
7710        let tok = self.consume(TokenType::Socket)?;
7711        let name = self.consume(TokenType::Identifier)?.value;
7712        let mut node = SocketDefinition {
7713            name,
7714            loc: Loc { line: tok.line, column: tok.column },
7715            ..Default::default()
7716        };
7717        self.consume(TokenType::LBrace)?;
7718        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7719            let key = self.consume_any_ident_or_kw()?.value;
7720            self.consume(TokenType::Colon)?;
7721            match key.as_str() {
7722                "protocol" => node.protocol = self.consume_any_ident_or_kw()?.value,
7723                "backpressure" => {
7724                    // `credit(n)` — the typed-resource window.
7725                    let kind = self.consume_any_ident_or_kw()?.value;
7726                    if kind != "credit" {
7727                        return Err(self.error(&format!("expected `credit(n)` for backpressure, got `{kind}`")));
7728                    }
7729                    self.consume(TokenType::LParen)?;
7730                    let n = self
7731                        .consume(TokenType::Integer)?
7732                        .value
7733                        .parse::<i64>()
7734                        .map_err(|_| self.error("backpressure credit must be an integer"))?;
7735                    self.consume(TokenType::RParen)?;
7736                    node.backpressure_credit = Some(n);
7737                }
7738                "reconnect" => {
7739                    let mode = self.consume_any_ident_or_kw()?.value;
7740                    node.reconnect = mode == "cognitive_state";
7741                }
7742                "legal_basis" => node.legal_basis = Some(self.consume_any_ident_or_kw()?.value),
7743                other => return Err(self.error(&format!("unknown socket field `{other}`"))),
7744            }
7745            // Optional comma between fields.
7746            if self.check(TokenType::Comma) {
7747                self.consume(TokenType::Comma)?;
7748            }
7749        }
7750        self.consume(TokenType::RBrace)?;
7751        Ok(node)
7752    }
7753
7754    /// §Fase 80.b — parse `upstream Name [from Preset@vN] { fields }`.
7755    ///
7756    /// Field grammar per `docs/fase/fase_80_upstream_design.md` §1–2. The
7757    /// parser fixes the *shape* only; catalog membership (`transport:`,
7758    /// `auth:`, `overflow:`, `on_exhausted:`), key charsets and projection
7759    /// totality are §80.c type-checker laws (T849–T851), mirroring how
7760    /// `socket` splits parse vs. check.
7761    fn parse_upstream(&mut self) -> Result<UpstreamDefinition, ParseError> {
7762        let tok = self.consume(TokenType::Upstream)?;
7763        let name = self.consume(TokenType::Identifier)?.value;
7764        let mut node = UpstreamDefinition {
7765            name,
7766            loc: Loc { line: tok.line, column: tok.column },
7767            ..Default::default()
7768        };
7769        // §80.f — preset instantiation: `upstream X from DeepgramSTT@v1 {…}`.
7770        if self.check(TokenType::From) {
7771            self.advance();
7772            let base = self.consume(TokenType::Identifier)?.value;
7773            self.consume(TokenType::At)?;
7774            let version = self.consume_any_ident_or_kw()?.value;
7775            node.preset = Some(format!("{base}@{version}"));
7776        }
7777        self.consume(TokenType::LBrace)?;
7778        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7779            let key = self.consume_any_ident_or_kw()?.value;
7780            self.consume(TokenType::Colon)?;
7781            match key.as_str() {
7782                "transport" => node.transport = self.consume_any_ident_or_kw()?.value,
7783                "protocol" => node.protocol = self.consume_any_ident_or_kw()?.value,
7784                "role" => node.role = self.consume_any_ident_or_kw()?.value,
7785                "resolve" => node.resolve = self.parse_dotted_identifier()?,
7786                // §Fase 114.u — the upstream's channel rides a declared
7787                // `resource`; the address + instance bound DERIVE from it.
7788                // XOR with `resolve:` is axon-T951 (type-checker territory).
7789                "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
7790                "secret" => node.secret = self.parse_dotted_identifier()?,
7791                "auth" => {
7792                    // `header("Name")` | `header("Name", "Prefix ")` |
7793                    // `query("param")` | `signed_url`.
7794                    node.auth_kind = self.consume_any_ident_or_kw()?.value;
7795                    if self.check(TokenType::LParen) {
7796                        self.consume(TokenType::LParen)?;
7797                        node.auth_name = Some(self.consume(TokenType::StringLit)?.value);
7798                        if self.check(TokenType::Comma) {
7799                            self.consume(TokenType::Comma)?;
7800                            node.auth_prefix = Some(self.consume(TokenType::StringLit)?.value);
7801                        }
7802                        self.consume(TokenType::RParen)?;
7803                    }
7804                }
7805                "map" => node.map = self.parse_upstream_map()?,
7806                "reconnect" => node.reconnect = Some(self.parse_upstream_reconnect()?),
7807                "overflow" => node.overflow = Some(self.consume_any_ident_or_kw()?.value),
7808                "backpressure" => {
7809                    // `credit(n)` — same typed-resource window as `socket`.
7810                    let kind = self.consume_any_ident_or_kw()?.value;
7811                    if kind != "credit" {
7812                        return Err(self.error(&format!("expected `credit(n)` for backpressure, got `{kind}`")));
7813                    }
7814                    self.consume(TokenType::LParen)?;
7815                    let n = self
7816                        .consume(TokenType::Integer)?
7817                        .value
7818                        .parse::<i64>()
7819                        .map_err(|_| self.error("backpressure credit must be an integer"))?;
7820                    self.consume(TokenType::RParen)?;
7821                    node.backpressure_credit = Some(n);
7822                }
7823                other => return Err(self.error(&format!("unknown upstream field `{other}`"))),
7824            }
7825            // Optional comma between fields.
7826            if self.check(TokenType::Comma) {
7827                self.consume(TokenType::Comma)?;
7828            }
7829        }
7830        self.consume(TokenType::RBrace)?;
7831        Ok(node)
7832    }
7833
7834    /// §Fase 83.a — parse `cors Name { fields }`. Field-shape checks
7835    /// (wildcard+credentials, origin-glob shape, closed method catalog,
7836    /// cross-method path consistency) are §83.c type-checker territory
7837    /// (T853-T857); the parser only builds the structural AST.
7838    ///
7839    /// **Unknown fields are a hard error** (D83.7, not `shield`'s lenient
7840    /// `axon-W010` record-and-skip) — mirrors `upstream`'s stricter
7841    /// posture, appropriate for a security-relevant declaration.
7842    fn parse_cors(&mut self) -> Result<CorsDefinition, ParseError> {
7843        let tok = self.consume(TokenType::Cors)?;
7844        let name = self.consume(TokenType::Identifier)?.value;
7845        let mut node = CorsDefinition {
7846            name,
7847            loc: Loc { line: tok.line, column: tok.column },
7848            ..Default::default()
7849        };
7850        self.consume(TokenType::LBrace)?;
7851        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7852            let key = self.consume_any_ident_or_kw()?.value;
7853            self.consume(TokenType::Colon)?;
7854            match key.as_str() {
7855                "allow_origins" => node.allow_origins = self.parse_bracketed_strings()?,
7856                "allow_methods" => node.allow_methods = self.parse_bracketed_identifiers()?,
7857                "allow_headers" => node.allow_headers = self.parse_bracketed_strings()?,
7858                "allow_credentials" => {
7859                    node.allow_credentials = self.consume_any_ident_or_kw()?.value == "true"
7860                }
7861                "max_age" => node.max_age = Some(self.consume(TokenType::Duration)?.value),
7862                "expose_headers" => node.expose_headers = self.parse_bracketed_strings()?,
7863                other => return Err(self.error(&format!("unknown cors field `{other}`"))),
7864            }
7865            // Optional comma between fields.
7866            if self.check(TokenType::Comma) {
7867                self.consume(TokenType::Comma)?;
7868            }
7869        }
7870        self.consume(TokenType::RBrace)?;
7871        Ok(node)
7872    }
7873
7874    /// §Fase 92.a — parse `credential Name { ttl: grants: }`. Strict
7875    /// closed-catalog (unknown field is a hard error, the §83 D83.7
7876    /// discipline — a credential contract governs AUTHORITY, so a typo can
7877    /// never silently produce a permissive contract). `grants:` slugs are
7878    /// validated at parse time with the same closed grammar as
7879    /// `axonendpoint requires:`; the cross-field laws (non-empty grants,
7880    /// TTL bounds) are §92.a type-checker territory (`axon-T893`/`T894`).
7881    fn parse_credential(&mut self) -> Result<CredentialDefinition, ParseError> {
7882        let tok = self.consume(TokenType::Credential)?;
7883        let name = self.consume(TokenType::Identifier)?.value;
7884        let mut node = CredentialDefinition {
7885            name,
7886            loc: Loc { line: tok.line, column: tok.column },
7887            ..Default::default()
7888        };
7889        self.consume(TokenType::LBrace)?;
7890        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7891            let key = self.consume_any_ident_or_kw()?.value;
7892            self.consume(TokenType::Colon)?;
7893            match key.as_str() {
7894                "ttl" => node.ttl = self.consume(TokenType::Duration)?.value,
7895                "grants" => {
7896                    let bracket_tok = self.current().clone();
7897                    let items = self.parse_bracketed_dot_identifiers()?;
7898                    for slug in &items {
7899                        if !is_valid_capability_slug(slug) {
7900                            return Err(ParseError {
7901                                message: format!(
7902                                    "Invalid capability slug '{slug}' in credential '{}' \
7903                                     `grants:`. Capability slugs must match \
7904                                     ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
7905                                     lowercase identifiers starting with a letter. Examples: \
7906                                     `chat.invoke`, `flow.execute`.",
7907                                    node.name
7908                                ),
7909                                line: bracket_tok.line,
7910                                column: bracket_tok.column,
7911                                ..Default::default()
7912                            });
7913                        }
7914                    }
7915                    node.grants = items;
7916                }
7917                other => return Err(self.error(&format!("unknown credential field `{other}`"))),
7918            }
7919            // Optional comma between fields.
7920            if self.check(TokenType::Comma) {
7921                self.consume(TokenType::Comma)?;
7922            }
7923        }
7924        self.consume(TokenType::RBrace)?;
7925        Ok(node)
7926    }
7927
7928    /// §Fase 85.a — parse `cache Name { backend:, ttl:, key:, default:,
7929    /// apply_to_effects:, invalidate_on: }`. Strict closed-catalog (unknown
7930    /// field is a hard error, the §83 D83.7 discipline — a cache governs
7931    /// correctness, so a typo can never silently mean "no policy"). All
7932    /// cross-field laws (single default, non-pure-needs-ttl, reference
7933    /// resolution, effect widening) are §85.c type-checker territory.
7934    fn parse_cache(&mut self) -> Result<CacheDefinition, ParseError> {
7935        let tok = self.consume(TokenType::Cache)?;
7936        let name = self.consume(TokenType::Identifier)?.value;
7937        let mut node = CacheDefinition {
7938            name,
7939            loc: Loc { line: tok.line, column: tok.column },
7940            ..Default::default()
7941        };
7942        self.consume(TokenType::LBrace)?;
7943        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7944            let key = self.consume_any_ident_or_kw()?.value;
7945            self.consume(TokenType::Colon)?;
7946            match key.as_str() {
7947                "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
7948                "ttl" => node.ttl = Some(self.consume(TokenType::Duration)?.value),
7949                "key" => node.key_params = self.parse_bracketed_identifiers()?,
7950                "default" => {
7951                    node.default_policy = self.consume_any_ident_or_kw()?.value == "true"
7952                }
7953                "apply_to_effects" => {
7954                    node.apply_to_effects = self.parse_bracketed_identifiers()?
7955                }
7956                "invalidate_on" => node.invalidate_on = self.parse_bracketed_identifiers()?,
7957                other => return Err(self.error(&format!("unknown cache field `{other}`"))),
7958            }
7959            if self.check(TokenType::Comma) {
7960                self.consume(TokenType::Comma)?;
7961            }
7962        }
7963        self.consume(TokenType::RBrace)?;
7964        Ok(node)
7965    }
7966
7967    // ── §Fase 99.b — Native Document Synthesis ─────────────────────────────
7968
7969    /// §Fase 99.b — parse `document <Name> { target:, template:?, provenance:?,
7970    /// effects:?, <body blocks> }`. Document-level scalars are handled here;
7971    /// anything of the form `ident { … }` is a body block ([`parse_doc_block_body`]).
7972    /// Unknown scalar fields are a hard error (the §83/§84 closed-catalog
7973    /// discipline); the per-`target` block vocabulary is the §99.c checker's job.
7974    fn parse_document(&mut self) -> Result<crate::ast::DocumentDefinition, ParseError> {
7975        let tok = self.consume(TokenType::Document)?;
7976        let name = self.consume(TokenType::Identifier)?.value;
7977        let mut node = crate::ast::DocumentDefinition {
7978            name,
7979            loc: Loc {
7980                line: tok.line,
7981                column: tok.column,
7982            },
7983            ..Default::default()
7984        };
7985        self.consume(TokenType::LBrace)?;
7986        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7987            let field = self.current().clone();
7988            let field_name = field.value.clone();
7989            self.advance();
7990            if self.check(TokenType::Colon) {
7991                self.advance();
7992                match field_name.as_str() {
7993                    "target" => node.target = self.consume_any_ident_or_kw()?.value,
7994                    "template" => node.template = self.parse_dotted_identifier()?,
7995                    "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
7996                    "effects" => node.effects = Some(self.parse_effect_row()?),
7997                    other => {
7998                        return Err(self.error(&format!(
7999                            "unknown document field `{other}` in document `{}` — expected \
8000                             `target:` / `template:` / `provenance:` / `effects:`, or a body \
8001                             block (`section {{ … }}` / `slide {{ … }}` / `sheet {{ … }}`)",
8002                            node.name
8003                        )))
8004                    }
8005                }
8006            } else if self.check(TokenType::LBrace) {
8007                node.blocks
8008                    .push(self.parse_doc_block_body(field_name, field.line, field.column)?);
8009            } else {
8010                return Err(self.error(&format!(
8011                    "unexpected `{field_name}` in document `{}` body — expected a `field:` or a \
8012                     body block `{field_name} {{ … }}`",
8013                    node.name
8014                )));
8015            }
8016            if self.check(TokenType::Comma) {
8017                self.advance();
8018            }
8019        }
8020        self.consume(TokenType::RBrace)?;
8021        Ok(node)
8022    }
8023
8024    /// §Fase 99.b — parse a document body block whose `kind` was already
8025    /// consumed: `{ (field: value | nested-block { … })* }`. Recursive — a
8026    /// `section` holds `para`/`table`/`chart`; a `slide` holds `bullets`/
8027    /// `notes`; a `sheet` holds `row`/`formula`. A member is a field iff a
8028    /// `:` follows its name; else it must open a nested block (`{`).
8029    fn parse_doc_block_body(
8030        &mut self,
8031        kind: String,
8032        line: u32,
8033        column: u32,
8034    ) -> Result<crate::ast::DocBlock, ParseError> {
8035        let mut block = crate::ast::DocBlock {
8036            kind,
8037            loc: Loc { line, column },
8038            ..Default::default()
8039        };
8040        self.consume(TokenType::LBrace)?;
8041        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8042            let name_tok = self.current().clone();
8043            let name = self.consume_any_ident_or_kw()?.value;
8044            if self.check(TokenType::Colon) {
8045                self.advance();
8046                let value = self.parse_doc_scalar()?;
8047                block.fields.push((name, value));
8048            } else if self.check(TokenType::LBrace) {
8049                let child = self.parse_doc_block_body(name, name_tok.line, name_tok.column)?;
8050                block.children.push(child);
8051            } else {
8052                return Err(self.error(&format!(
8053                    "in document block `{}`: `{name}` must be a `field:` value or open a nested \
8054                     block `{name} {{ … }}`",
8055                    block.kind
8056                )));
8057            }
8058            if self.check(TokenType::Comma) {
8059                self.advance();
8060            }
8061        }
8062        self.consume(TokenType::RBrace)?;
8063        Ok(block)
8064    }
8065
8066    /// §Fase 99.b — parse a document field value into a [`crate::ast::DocScalar`].
8067    /// A bare identifier is a REFERENCE (`text: revenue_summary`) — this is what
8068    /// the assertion-laundering barrier inspects; a quoted string / int / bool /
8069    /// bracketed list are literals.
8070    fn parse_doc_scalar(&mut self) -> Result<crate::ast::DocScalar, ParseError> {
8071        let tok = self.current().clone();
8072        match tok.ttype {
8073            TokenType::StringLit => {
8074                self.advance();
8075                Ok(crate::ast::DocScalar::Text(tok.value))
8076            }
8077            TokenType::Integer => {
8078                self.advance();
8079                Ok(crate::ast::DocScalar::Int(tok.value.parse::<i64>().unwrap_or(0)))
8080            }
8081            TokenType::Bool => {
8082                self.advance();
8083                Ok(crate::ast::DocScalar::Bool(tok.value == "true"))
8084            }
8085            TokenType::LBracket => {
8086                let items = self.parse_bracketed_strings()?;
8087                Ok(crate::ast::DocScalar::List(items))
8088            }
8089            _ => {
8090                let name = self.consume_any_ident_or_kw()?.value;
8091                Ok(crate::ast::DocScalar::Ref(name))
8092            }
8093        }
8094    }
8095
8096    // ── §Fase 105 — Governed CRM Delivery ──────────────────────────────────
8097
8098    /// §Fase 105 — parse `deliver <Name> { target:, provenance:?, secret:,
8099    /// effects:?, <operation blocks> }`. Delivery-level scalars are handled here;
8100    /// anything of the form `ident { … }` is an operation block
8101    /// ([`parse_deliver_op`]). Unknown scalar fields are a hard error (the §99
8102    /// §Fase 110.a — the governed human-notification declaration:
8103    ///
8104    /// ```text
8105    /// notify LowSales {
8106    ///     channel:    sms | whatsapp | telegram
8107    ///     to:         secret(ops.oncall_phone)
8108    ///     template:   "Ventas 7d: ${resumen}"
8109    ///     window:     4h
8110    ///     provenance: attached | cleared
8111    ///     effects:    <web>
8112    /// }
8113    /// ```
8114    ///
8115    /// The closed-field discipline (§99/§105): an unknown scalar field is
8116    /// a hard parse error. The LAWS (T933/T934/T935) live in the checker
8117    /// so violations accumulate; the parser records shape (including a
8118    /// literal `to:` — kept so T934 can refuse it TEACHING the custody
8119    /// form, instead of a bare parse error).
8120    fn parse_notify(&mut self) -> Result<crate::ast::NotifyDefinition, ParseError> {
8121        let tok = self.consume(TokenType::Notify)?;
8122        let name = self.consume(TokenType::Identifier)?.value;
8123        let mut node = crate::ast::NotifyDefinition {
8124            name,
8125            loc: Loc {
8126                line: tok.line,
8127                column: tok.column,
8128            },
8129            ..Default::default()
8130        };
8131        self.consume(TokenType::LBrace)?;
8132        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8133            let field = self.current().clone();
8134            let field_name = field.value.clone();
8135            self.advance();
8136            if self.check(TokenType::Colon) {
8137                self.advance();
8138                match field_name.as_str() {
8139                    "channel" => node.channel = self.consume_any_ident_or_kw()?.value,
8140                    "to" => {
8141                        // The custody form: `secret(<dotted-class>)`. A string
8142                        // literal parses too — the checker refuses it (T934)
8143                        // with the teaching message.
8144                        if self.current().value == "secret" && self.peek_is_lparen() {
8145                            self.advance(); // `secret`
8146                            self.consume(TokenType::LParen)?;
8147                            node.to_secret = self.parse_dotted_identifier()?;
8148                            self.consume(TokenType::RParen)?;
8149                            node.to_is_secret = true;
8150                        } else if self.check(TokenType::StringLit) {
8151                            node.to_secret = self.consume(TokenType::StringLit)?.value.clone();
8152                            node.to_is_secret = false;
8153                        } else {
8154                            node.to_secret = self.consume_any_ident_or_kw()?.value.clone();
8155                            node.to_is_secret = false;
8156                        }
8157                    }
8158                    "template" => {
8159                        node.template = self.consume(TokenType::StringLit)?.value.clone()
8160                    }
8161                    "window" => {
8162                        // `4h` lexes as Integer + ident or one ident — accept
8163                        // both spellings, normalized to the joined form.
8164                        if self.check(TokenType::Integer) {
8165                            let n = self.consume(TokenType::Integer)?.value.clone();
8166                            let unit = self.consume_any_ident_or_kw()?.value.clone();
8167                            node.window = format!("{n}{unit}");
8168                        } else {
8169                            node.window = self.consume_any_ident_or_kw()?.value.clone();
8170                        }
8171                    }
8172                    "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
8173                    "effects" => node.effects = Some(self.parse_effect_row()?),
8174                    other => {
8175                        return Err(self.error(&format!(
8176                            "unknown notify field `{other}` in notify `{}` — expected \
8177                             `channel:` / `to:` / `template:` / `window:` / `provenance:` / \
8178                             `effects:`",
8179                            node.name
8180                        )))
8181                    }
8182                }
8183            }
8184        }
8185        self.consume(TokenType::RBrace)?;
8186        Ok(node)
8187    }
8188
8189    /// §Fase 110.a — one-token lookahead helper for the `secret(` form.
8190    /// §Fase 114.a — is the NEXT token an identifier? (`budget <Name> { … }` vs
8191    /// a bare `budget` used as an ordinary identifier.)
8192    fn peek_is_identifier(&self) -> bool {
8193        self.tokens
8194            .get(self.pos + 1)
8195            .map(|t| t.ttype == TokenType::Identifier)
8196            .unwrap_or(false)
8197    }
8198
8199    fn peek_is_lparen(&self) -> bool {
8200        self.tokens
8201            .get(self.pos + 1)
8202            .map(|t| t.ttype == TokenType::LParen)
8203            .unwrap_or(false)
8204    }
8205
8206    /// closed-catalog discipline); the operation vocabulary is the checker's job.
8207    fn parse_deliver(&mut self) -> Result<crate::ast::DeliverDefinition, ParseError> {
8208        let tok = self.consume(TokenType::Deliver)?;
8209        let name = self.consume(TokenType::Identifier)?.value;
8210        let mut node = crate::ast::DeliverDefinition {
8211            name,
8212            loc: Loc {
8213                line: tok.line,
8214                column: tok.column,
8215            },
8216            ..Default::default()
8217        };
8218        self.consume(TokenType::LBrace)?;
8219        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8220            let field = self.current().clone();
8221            let field_name = field.value.clone();
8222            self.advance();
8223            if self.check(TokenType::Colon) {
8224                self.advance();
8225                match field_name.as_str() {
8226                    "target" => node.target = self.consume_any_ident_or_kw()?.value,
8227                    "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
8228                    "secret" => node.secret = self.consume_any_ident_or_kw()?.value,
8229                    "effects" => node.effects = Some(self.parse_effect_row()?),
8230                    other => {
8231                        return Err(self.error(&format!(
8232                            "unknown deliver field `{other}` in deliver `{}` — expected \
8233                             `target:` / `provenance:` / `secret:` / `effects:`, or an operation \
8234                             block (`upsert_contact {{ … }}` / `create_deal {{ … }}` / \
8235                             `add_note {{ … }}`)",
8236                            node.name
8237                        )))
8238                    }
8239                }
8240            } else if self.check(TokenType::LBrace) {
8241                node.ops
8242                    .push(self.parse_deliver_op(field_name, field.line, field.column)?);
8243            } else {
8244                return Err(self.error(&format!(
8245                    "unexpected `{field_name}` in deliver `{}` body — expected a `field:` or an \
8246                     operation block `{field_name} {{ … }}`",
8247                    node.name
8248                )));
8249            }
8250            if self.check(TokenType::Comma) {
8251                self.advance();
8252            }
8253        }
8254        self.consume(TokenType::RBrace)?;
8255        Ok(node)
8256    }
8257
8258    /// §Fase 105 — parse a delivery operation block whose `kind` was already
8259    /// consumed: `{ (field: value)* }`. Flat (unlike a document block, an
8260    /// operation has no nested children) — each member must be a `field: value`.
8261    fn parse_deliver_op(
8262        &mut self,
8263        kind: String,
8264        line: u32,
8265        column: u32,
8266    ) -> Result<crate::ast::DeliverOp, ParseError> {
8267        let mut op = crate::ast::DeliverOp {
8268            kind,
8269            loc: Loc { line, column },
8270            ..Default::default()
8271        };
8272        self.consume(TokenType::LBrace)?;
8273        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8274            let name = self.consume_any_ident_or_kw()?.value;
8275            self.consume(TokenType::Colon).map_err(|_| {
8276                self.error(&format!(
8277                    "in deliver operation `{}`: `{name}` must be a `field: value` pair — an \
8278                     operation binds scalar fields, it takes no nested blocks",
8279                    op.kind
8280                ))
8281            })?;
8282            let value = self.parse_doc_scalar()?;
8283            op.fields.push((name, value));
8284            if self.check(TokenType::Comma) {
8285                self.advance();
8286            }
8287        }
8288        self.consume(TokenType::RBrace)?;
8289        Ok(op)
8290    }
8291
8292    /// §Fase 87.a — parse `savant <Name> { domain:, cognition{…}, memory{…},
8293    /// budget{…}, mandate <M> {…} … }`. The block surface only; catalog +
8294    /// ref-resolution + budget/interruptibility binding is the §87.b/c checker's
8295    /// job (the standing parse/check split). Unknown fields are a hard error
8296    /// (D83.7): a savant governs an expensive autonomous process.
8297    fn parse_savant(&mut self) -> Result<SavantDefinition, ParseError> {
8298        let tok = self.consume(TokenType::Savant)?;
8299        let name = self.consume(TokenType::Identifier)?.value;
8300        let mut node = SavantDefinition {
8301            name,
8302            loc: Loc {
8303                line: tok.line,
8304                column: tok.column,
8305            },
8306            ..Default::default()
8307        };
8308        self.consume(TokenType::LBrace)?;
8309        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8310            let field = self.current().clone();
8311            let field_name = field.value.clone();
8312            self.advance();
8313            if self.check(TokenType::Colon) {
8314                self.advance();
8315                match field_name.as_str() {
8316                    "domain" => node.domain = self.consume(TokenType::StringLit)?.value,
8317                    other => {
8318                        return Err(self.error(&format!(
8319                            "unknown savant field `{other}` in savant `{}` — expected \
8320                             `domain:` or a `cognition` / `memory` / `budget` / `mandate` block",
8321                            node.name
8322                        )))
8323                    }
8324                }
8325            } else if field_name == "cognition" {
8326                node.cognition = Some(self.parse_savant_cognition(field.line, field.column)?);
8327            } else if field_name == "memory" {
8328                node.memory = Some(self.parse_savant_memory(field.line, field.column)?);
8329            } else if field_name == "budget" {
8330                node.budget = Some(self.parse_savant_budget(field.line, field.column)?);
8331            } else if field_name == "mandate" {
8332                node.mandates
8333                    .push(self.parse_savant_mandate(field.line, field.column)?);
8334            } else {
8335                return Err(self.error(&format!(
8336                    "unexpected `{field_name}` in savant `{}` body — expected `domain:` or a \
8337                     `cognition` / `memory` / `budget` / `mandate` block",
8338                    node.name
8339                )));
8340            }
8341            if self.check(TokenType::Comma) {
8342                self.advance();
8343            }
8344        }
8345        self.consume(TokenType::RBrace)?;
8346        Ok(node)
8347    }
8348
8349    /// §Fase 87.a — the `cognition { depth:, entropic_threshold:, divergence: }`
8350    /// sub-block. Catalog validation of `depth`/`divergence` is §87.b.
8351    fn parse_savant_cognition(
8352        &mut self,
8353        line: u32,
8354        column: u32,
8355    ) -> Result<SavantCognition, ParseError> {
8356        self.consume(TokenType::LBrace)?;
8357        let mut node = SavantCognition {
8358            loc: Loc { line, column },
8359            ..Default::default()
8360        };
8361        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8362            let key = self.consume_any_ident_or_kw()?.value;
8363            self.consume(TokenType::Colon)?;
8364            match key.as_str() {
8365                "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
8366                "entropic_threshold" => node.entropic_threshold = self.parse_optional_float(),
8367                "divergence" => node.divergence = self.consume_any_ident_or_kw()?.value,
8368                other => {
8369                    return Err(self.error(&format!(
8370                        "unknown savant `cognition` field `{other}` — expected \
8371                         `depth` / `entropic_threshold` / `divergence`"
8372                    )))
8373                }
8374            }
8375            if self.check(TokenType::Comma) {
8376                self.advance();
8377            }
8378        }
8379        self.consume(TokenType::RBrace)?;
8380        Ok(node)
8381    }
8382
8383    /// §Fase 87.a — the `memory { backend:, corpus_graph:, isolation_level: }`
8384    /// sub-block. `backend` is resolved to a declared `memory`/`corpus` in §87.c.
8385    fn parse_savant_memory(
8386        &mut self,
8387        line: u32,
8388        column: u32,
8389    ) -> Result<SavantMemory, ParseError> {
8390        self.consume(TokenType::LBrace)?;
8391        let mut node = SavantMemory {
8392            loc: Loc { line, column },
8393            ..Default::default()
8394        };
8395        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8396            let key = self.consume_any_ident_or_kw()?.value;
8397            self.consume(TokenType::Colon)?;
8398            match key.as_str() {
8399                "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
8400                "corpus_graph" => {
8401                    node.corpus_graph = self.consume_any_ident_or_kw()?.value == "true"
8402                }
8403                "isolation_level" => node.isolation_level = self.consume_any_ident_or_kw()?.value,
8404                other => {
8405                    return Err(self.error(&format!(
8406                        "unknown savant `memory` field `{other}` — expected \
8407                         `backend` / `corpus_graph` / `isolation_level`"
8408                    )))
8409                }
8410            }
8411            if self.check(TokenType::Comma) {
8412                self.advance();
8413            }
8414        }
8415        self.consume(TokenType::RBrace)?;
8416        Ok(node)
8417    }
8418
8419    /// §Fase 87.a — the `budget { max_iterations:, max_tool_synth: }` sub-block.
8420    /// Bound to a §72 linear budget (`RateLease`) in §87.c.
8421    fn parse_savant_budget(
8422        &mut self,
8423        line: u32,
8424        column: u32,
8425    ) -> Result<SavantBudget, ParseError> {
8426        self.consume(TokenType::LBrace)?;
8427        let mut node = SavantBudget {
8428            loc: Loc { line, column },
8429            ..Default::default()
8430        };
8431        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8432            let key = self.consume_any_ident_or_kw()?.value;
8433            self.consume(TokenType::Colon)?;
8434            match key.as_str() {
8435                "max_iterations" => node.max_iterations = self.parse_optional_int(),
8436                "max_tool_synth" => node.max_tool_synth = self.parse_optional_int(),
8437                other => {
8438                    return Err(self.error(&format!(
8439                        "unknown savant `budget` field `{other}` — expected \
8440                         `max_iterations` / `max_tool_synth`"
8441                    )))
8442                }
8443            }
8444            if self.check(TokenType::Comma) {
8445                self.advance();
8446            }
8447        }
8448        self.consume(TokenType::RBrace)?;
8449        Ok(node)
8450    }
8451
8452    /// §Fase 87.a — the `mandate <Name> { objective:, output: }` sub-block. The
8453    /// `mandate` keyword is already consumed by `parse_savant`.
8454    fn parse_savant_mandate(
8455        &mut self,
8456        line: u32,
8457        column: u32,
8458    ) -> Result<SavantMandate, ParseError> {
8459        let name = self.consume(TokenType::Identifier)?.value;
8460        let mut node = SavantMandate {
8461            name,
8462            loc: Loc { line, column },
8463            ..Default::default()
8464        };
8465        self.consume(TokenType::LBrace)?;
8466        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8467            let key = self.consume_any_ident_or_kw()?.value;
8468            self.consume(TokenType::Colon)?;
8469            match key.as_str() {
8470                "objective" => node.objective = self.consume(TokenType::StringLit)?.value,
8471                "output" => node.output_type = self.consume_any_ident_or_kw()?.value,
8472                other => {
8473                    return Err(self.error(&format!(
8474                        "unknown savant `mandate` field `{other}` — expected `objective` / `output`"
8475                    )))
8476                }
8477            }
8478            if self.check(TokenType::Comma) {
8479                self.advance();
8480            }
8481        }
8482        self.consume(TokenType::RBrace)?;
8483        Ok(node)
8484    }
8485
8486    /// §Fase 87.d — parse `synth <Name> { target:, risk:, language:, sandbox:,
8487    /// review:, max_lines: }`. Flat key:value block (the `cache` shape). Catalog
8488    /// + deny-by-default validation is §87.d `check_synth`. Unknown fields are a
8489    /// hard error (D83.7): a synth policy governs arbitrary-code execution.
8490    fn parse_synth(&mut self) -> Result<SynthDefinition, ParseError> {
8491        let tok = self.consume(TokenType::Synth)?;
8492        let name = self.consume(TokenType::Identifier)?.value;
8493        let mut node = SynthDefinition {
8494            name,
8495            loc: Loc {
8496                line: tok.line,
8497                column: tok.column,
8498            },
8499            ..Default::default()
8500        };
8501        self.consume(TokenType::LBrace)?;
8502        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8503            let key = self.consume_any_ident_or_kw()?.value;
8504            self.consume(TokenType::Colon)?;
8505            match key.as_str() {
8506                "target" => node.target = self.consume(TokenType::StringLit)?.value,
8507                "risk" => node.risk = self.consume_any_ident_or_kw()?.value,
8508                "language" => node.language = self.consume_any_ident_or_kw()?.value,
8509                "sandbox" => node.sandbox = self.consume_any_ident_or_kw()?.value,
8510                "review" => node.review = self.consume_any_ident_or_kw()?.value,
8511                "max_lines" => node.max_lines = self.parse_optional_int(),
8512                other => {
8513                    return Err(self.error(&format!(
8514                        "unknown synth field `{other}` in synth `{}` — expected `target` / `risk` \
8515                         / `language` / `sandbox` / `review` / `max_lines`",
8516                        node.name
8517                    )))
8518                }
8519            }
8520            if self.check(TokenType::Comma) {
8521                self.consume(TokenType::Comma)?;
8522            }
8523        }
8524        self.consume(TokenType::RBrace)?;
8525        Ok(node)
8526    }
8527
8528    /// §Fase 80.g — parse `voice Name { fields }`. Cross-field laws
8529    /// (stt/tts XOR realtime, interruptible ⇒ legal_basis, ref resolution)
8530    /// are §80.c type-checker territory (T852), same parse/check split as
8531    /// every primitive in this file.
8532    fn parse_voice(&mut self) -> Result<VoiceDefinition, ParseError> {
8533        let tok = self.consume(TokenType::Voice)?;
8534        let name = self.consume(TokenType::Identifier)?.value;
8535        let mut node = VoiceDefinition {
8536            name,
8537            loc: Loc { line: tok.line, column: tok.column },
8538            ..Default::default()
8539        };
8540        self.consume(TokenType::LBrace)?;
8541        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8542            let key = self.consume_any_ident_or_kw()?.value;
8543            self.consume(TokenType::Colon)?;
8544            match key.as_str() {
8545                // Each leg: a declared upstream name or a `Preset@vN` ref.
8546                "stt" => node.stt = Some(self.parse_upstream_ref()?),
8547                "tts" => node.tts = Some(self.parse_upstream_ref()?),
8548                "realtime" => node.realtime = Some(self.parse_upstream_ref()?),
8549                "carrier" => node.carrier = self.consume_any_ident_or_kw()?.value,
8550                "interruptible" => {
8551                    let v = self.consume_any_ident_or_kw()?.value;
8552                    node.interruptible = v == "true";
8553                }
8554                "legal_basis" => node.legal_basis = Some(self.consume_any_ident_or_kw()?.value),
8555                "persona" => node.persona = Some(self.consume(TokenType::Identifier)?.value),
8556                "context" => node.context = Some(self.consume(TokenType::Identifier)?.value),
8557                other => return Err(self.error(&format!("unknown voice field `{other}`"))),
8558            }
8559            if self.check(TokenType::Comma) {
8560                self.consume(TokenType::Comma)?;
8561            }
8562        }
8563        self.consume(TokenType::RBrace)?;
8564        Ok(node)
8565    }
8566
8567    /// §Fase 80.g — an upstream leg reference: `Ident` (a declared
8568    /// `upstream`) or `Ident@vN` (a §80.f preset).
8569    fn parse_upstream_ref(&mut self) -> Result<String, ParseError> {
8570        let base = self.consume(TokenType::Identifier)?.value;
8571        if self.check(TokenType::At) {
8572            self.advance();
8573            let version = self.consume_any_ident_or_kw()?.value;
8574            Ok(format!("{base}@{version}"))
8575        } else {
8576            Ok(base)
8577        }
8578    }
8579
8580    /// §Fase 80.b — parse the `map: [ rule, … ]` projection list.
8581    ///
8582    /// rule := (`send` | `receive`) <MessageType> `as` (`json` | `binary`)
8583    ///         [ `tag` <string> ]                 — send-json only
8584    ///         [ `when` <string> `=` <string> ]   — receive-json only
8585    fn parse_upstream_map(&mut self) -> Result<Vec<UpstreamMapRule>, ParseError> {
8586        self.consume(TokenType::LBracket)?;
8587        let mut rules = Vec::new();
8588        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
8589            let dir_tok = self.current().clone();
8590            let direction = match dir_tok.ttype {
8591                TokenType::Send => "send",
8592                TokenType::Receive => "receive",
8593                _ => {
8594                    return Err(self.error(&format!(
8595                        "upstream map rule must start with `send` or `receive`, got `{}`",
8596                        dir_tok.value
8597                    )))
8598                }
8599            };
8600            self.advance();
8601            let message = self.consume(TokenType::Identifier)?.value;
8602            self.consume(TokenType::As)?;
8603            let framing = self.consume_any_ident_or_kw()?.value;
8604            let mut rule = UpstreamMapRule {
8605                direction: direction.to_string(),
8606                message,
8607                framing,
8608                loc: Loc { line: dir_tok.line, column: dir_tok.column },
8609                ..Default::default()
8610            };
8611            // Optional selectors — contextual identifiers, not keywords.
8612            if self.current().value == "tag" {
8613                self.advance();
8614                rule.tag = Some(self.consume(TokenType::StringLit)?.value);
8615            } else if self.current().value == "when" {
8616                // `when "f" = "v"` — equality discriminator; `when "f"` —
8617                // field-PRESENCE discriminator (vendors like Gemini Live /
8618                // ElevenLabs mark frame kinds by which key exists, not by a
8619                // type value).
8620                self.advance();
8621                rule.when_field = Some(self.consume(TokenType::StringLit)?.value);
8622                if self.check(TokenType::Assign) {
8623                    self.advance();
8624                    rule.when_value = Some(self.consume(TokenType::StringLit)?.value);
8625                }
8626            }
8627            rules.push(rule);
8628            if self.check(TokenType::Comma) {
8629                self.advance();
8630            }
8631        }
8632        self.consume(TokenType::RBracket)?;
8633        Ok(rules)
8634    }
8635
8636    /// §Fase 80.b — parse `reconnect: { backoff_ms: <int>, max_attempts:
8637    /// <int>, on_exhausted: <ident> }` (order-free, all three required —
8638    /// a reconnection policy with a hole is not a policy).
8639    fn parse_upstream_reconnect(&mut self) -> Result<UpstreamReconnect, ParseError> {
8640        self.consume(TokenType::LBrace)?;
8641        let mut backoff_ms: Option<i64> = None;
8642        let mut max_attempts: Option<i64> = None;
8643        let mut on_exhausted: Option<String> = None;
8644        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8645            let key = self.consume_any_ident_or_kw()?.value;
8646            self.consume(TokenType::Colon)?;
8647            match key.as_str() {
8648                "backoff_ms" => {
8649                    backoff_ms = Some(
8650                        self.consume(TokenType::Integer)?
8651                            .value
8652                            .parse::<i64>()
8653                            .map_err(|_| self.error("backoff_ms must be an integer"))?,
8654                    )
8655                }
8656                "max_attempts" => {
8657                    max_attempts = Some(
8658                        self.consume(TokenType::Integer)?
8659                            .value
8660                            .parse::<i64>()
8661                            .map_err(|_| self.error("max_attempts must be an integer"))?,
8662                    )
8663                }
8664                "on_exhausted" => on_exhausted = Some(self.consume_any_ident_or_kw()?.value),
8665                other => return Err(self.error(&format!("unknown reconnect field `{other}`"))),
8666            }
8667            if self.check(TokenType::Comma) {
8668                self.consume(TokenType::Comma)?;
8669            }
8670        }
8671        self.consume(TokenType::RBrace)?;
8672        match (backoff_ms, max_attempts, on_exhausted) {
8673            (Some(b), Some(m), Some(o)) => Ok(UpstreamReconnect { backoff_ms: b, max_attempts: m, on_exhausted: o }),
8674            _ => Err(self.error(
8675                "reconnect requires all of `backoff_ms:`, `max_attempts:`, `on_exhausted:` — a reconnection policy with a hole is not a policy",
8676            )),
8677        }
8678    }
8679
8680    /// Parse: `[send T, receive U, loop, end]`.
8681    fn parse_session_steps(&mut self) -> Result<Vec<SessionStep>, ParseError> {
8682        self.consume(TokenType::LBracket)?;
8683        let mut steps = Vec::new();
8684        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
8685            steps.push(self.parse_session_step()?);
8686            if self.check(TokenType::Comma) {
8687                self.advance();
8688            }
8689        }
8690        self.consume(TokenType::RBracket)?;
8691        Ok(steps)
8692    }
8693
8694    /// §Fase 79.b — a **brace**-delimited session step block: `{ step, step, … }`.
8695    /// Used by the `interrupt`/`resumable` regions (the paper's block surface),
8696    /// as opposed to the `[ … ]` step-lists used by roles and choice arms.
8697    fn parse_session_step_block(&mut self) -> Result<Vec<SessionStep>, ParseError> {
8698        self.consume(TokenType::LBrace)?;
8699        let mut steps = Vec::new();
8700        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8701            steps.push(self.parse_session_step()?);
8702            if self.check(TokenType::Comma) {
8703                self.advance();
8704            }
8705        }
8706        self.consume(TokenType::RBrace)?;
8707        Ok(steps)
8708    }
8709
8710    fn parse_session_step(&mut self) -> Result<SessionStep, ParseError> {
8711        let tok = self.current().clone();
8712        let loc = Loc { line: tok.line, column: tok.column };
8713        match tok.ttype {
8714            TokenType::Send => {
8715                self.advance();
8716                let msg = self.consume_any_ident_or_kw()?;
8717                Ok(SessionStep { op: "send".into(), message_type: msg.value, loc, ..Default::default() })
8718            }
8719            TokenType::Receive => {
8720                self.advance();
8721                let msg = self.consume_any_ident_or_kw()?;
8722                Ok(SessionStep { op: "receive".into(), message_type: msg.value, loc, ..Default::default() })
8723            }
8724            TokenType::Loop => {
8725                self.advance();
8726                Ok(SessionStep { op: "loop".into(), loc, ..Default::default() })
8727            }
8728            TokenType::End => {
8729                self.advance();
8730                Ok(SessionStep { op: "end".into(), loc, ..Default::default() })
8731            }
8732            // §Fase 41.b — choice: `select { ℓ: [..], … }` (⊕) | `branch { ℓ: [..], … }` (&).
8733            // `select`/`branch` are not keywords — they arrive as identifiers.
8734            TokenType::Identifier if tok.value == "select" || tok.value == "branch" => {
8735                self.parse_session_choice(&tok.value, loc)
8736            }
8737            // §Fase 79.b — `interrupt { <body> } on <Signal> as <sig> resumable { <handler> }`.
8738            // Contextual keyword (identifier), like `select`/`branch`.
8739            TokenType::Identifier if tok.value == "interrupt" => {
8740                self.parse_session_interrupt(loc)
8741            }
8742            // §Fase 79.b — `resume`: the handler's normal exit (hand control back to
8743            // the parked body). A bare step, no payload; only meaningful inside an
8744            // `interrupt` handler (enforced at type-check, §79.c).
8745            TokenType::Identifier if tok.value == "resume" => {
8746                self.advance();
8747                Ok(SessionStep { op: "resume".into(), loc, ..Default::default() })
8748            }
8749            _ => Err(ParseError {
8750                message: format!(
8751                    "Invalid session step '{}' — expected send | receive | loop | end | select | branch | interrupt | resume",
8752                    tok.value
8753                ),
8754                line: tok.line,
8755                column: tok.column,
8756                ..Default::default()
8757            }),
8758        }
8759    }
8760
8761    /// §Fase 79.b — consume a **contextual keyword** (`on` / `as` / `resumable`):
8762    /// a token whose *value* must equal `kw`, regardless of whether the lexer
8763    /// classified it as a keyword or a bare identifier. Keeps the `interrupt`
8764    /// surface readable without minting three reserved words.
8765    fn consume_contextual(&mut self, kw: &str) -> Result<(), ParseError> {
8766        let t = self.current().clone();
8767        if t.value != kw {
8768            return Err(ParseError {
8769                message: format!("expected `{kw}` in interrupt step, got `{}`", t.value),
8770                line: t.line,
8771                column: t.column,
8772                ..Default::default()
8773            });
8774        }
8775        self.advance();
8776        Ok(())
8777    }
8778
8779    /// §Fase 79.b — Parse an interruptible region:
8780    /// `interrupt { <body-steps> } on <Signal> as <sig> resumable { <handler-steps> }`.
8781    ///
8782    /// Encoded into the string-tagged `SessionStep` (mirroring the §41.b choice
8783    /// shape): `op = "interrupt"`, `message_type = <Signal>` (validated against the
8784    /// closed `CallInterruptCause` catalog at type-check, §79.c), two labelled
8785    /// `branches` (`body`, `handler`), `binder = <sig>`, `resumable = true`.
8786    fn parse_session_interrupt(&mut self, loc: Loc) -> Result<SessionStep, ParseError> {
8787        self.advance(); // consume `interrupt`
8788        // Body region — a brace-delimited step block (the paper's `interrupt { … }`
8789        // surface; distinct from the `[ … ]` step-lists of roles/choice arms).
8790        let body = self.parse_session_step_block()?;
8791        // `on <Signal>`
8792        self.consume_contextual("on")?;
8793        let signal = self.consume_any_ident_or_kw()?;
8794        // `as <sig>`
8795        self.consume_contextual("as")?;
8796        let binder = self.consume_any_ident_or_kw()?;
8797        // `resumable { <handler> }`
8798        self.consume_contextual("resumable")?;
8799        let handler = self.parse_session_step_block()?;
8800        Ok(SessionStep {
8801            op: "interrupt".into(),
8802            message_type: signal.value,
8803            branches: vec![
8804                SessionBranch { label: "body".into(), steps: body, loc: loc.clone() },
8805                SessionBranch { label: "handler".into(), steps: handler, loc: loc.clone() },
8806            ],
8807            binder: binder.value,
8808            resumable: true,
8809            loc,
8810        })
8811    }
8812
8813    /// §Fase 41.b — Parse a choice step: `select { ask: [..], cancel: [..] }`
8814    /// (or `branch { … }`). Each `label: [steps]` arm is a nested sub-protocol.
8815    fn parse_session_choice(&mut self, op: &str, loc: Loc) -> Result<SessionStep, ParseError> {
8816        self.advance(); // consume `select` / `branch`
8817        self.consume(TokenType::LBrace)?;
8818        let mut branches = Vec::new();
8819        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8820            let label_tok = self.consume_any_ident_or_kw()?;
8821            self.consume(TokenType::Colon)?;
8822            let steps = self.parse_session_steps()?;
8823            branches.push(SessionBranch {
8824                label: label_tok.value,
8825                steps,
8826                loc: Loc { line: label_tok.line, column: label_tok.column },
8827            });
8828            if self.check(TokenType::Comma) {
8829                self.advance();
8830            }
8831        }
8832        self.consume(TokenType::RBrace)?;
8833        Ok(SessionStep { op: op.to_string(), branches, loc, ..Default::default() })
8834    }
8835
8836    /// Parse: `topology Name { nodes: [A, B, …]  edges: [A -> B : Session, …] }`.
8837    fn parse_topology(&mut self) -> Result<TopologyDefinition, ParseError> {
8838        let tok = self.consume(TokenType::Topology)?;
8839        let name = self.consume(TokenType::Identifier)?.value;
8840        let mut node = TopologyDefinition {
8841            name,
8842            nodes: Vec::new(),
8843            edges: Vec::new(),
8844            loc: Loc {
8845                line: tok.line,
8846                column: tok.column,
8847            },
8848            leading_trivia: Vec::new(),
8849            trailing_trivia: Vec::new(),
8850        };
8851        self.consume(TokenType::LBrace)?;
8852        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8853            let field_name = self.current().value.clone();
8854            self.advance();
8855            if !self.check(TokenType::Colon) {
8856                if self.check(TokenType::LBrace) {
8857                    self.skip_braced_block()?;
8858                }
8859                continue;
8860            }
8861            self.advance();
8862            match field_name.as_str() {
8863                "nodes" => node.nodes = self.parse_bracketed_identifiers()?,
8864                "edges" => node.edges = self.parse_topology_edges()?,
8865                _ => self.skip_value(),
8866            }
8867        }
8868        self.consume(TokenType::RBrace)?;
8869        Ok(node)
8870    }
8871
8872    fn parse_topology_edges(&mut self) -> Result<Vec<TopologyEdge>, ParseError> {
8873        self.consume(TokenType::LBracket)?;
8874        let mut edges = Vec::new();
8875        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
8876            edges.push(self.parse_topology_edge()?);
8877            if self.check(TokenType::Comma) {
8878                self.advance();
8879            }
8880        }
8881        self.consume(TokenType::RBracket)?;
8882        Ok(edges)
8883    }
8884
8885    fn parse_topology_edge(&mut self) -> Result<TopologyEdge, ParseError> {
8886        let src_tok = self.consume_any_ident_or_kw()?;
8887        self.consume(TokenType::Arrow)?;
8888        let tgt_tok = self.consume_any_ident_or_kw()?;
8889        self.consume(TokenType::Colon)?;
8890        let sess_tok = self.consume_any_ident_or_kw()?;
8891        Ok(TopologyEdge {
8892            source: src_tok.value,
8893            target: tgt_tok.value,
8894            session_ref: sess_tok.value,
8895            loc: Loc {
8896                line: src_tok.line,
8897                column: src_tok.column,
8898            },
8899        })
8900    }
8901
8902    // ── §λ-L-E Fase 5 — Cognitive immune system (paper_immune_v2.md) ────
8903
8904    /// Parse: `immune Name { watch, sensitivity, baseline, window, scope, tau, decay }`.
8905    fn parse_immune(&mut self) -> Result<ImmuneDefinition, ParseError> {
8906        let tok = self.consume(TokenType::Immune)?;
8907        let name = self.consume(TokenType::Identifier)?.value;
8908        let mut node = ImmuneDefinition {
8909            name,
8910            watch: Vec::new(),
8911            sensitivity: None,
8912            baseline: "learned".to_string(),
8913            window: 100,
8914            scope: String::new(),
8915            tau: String::new(),
8916            decay: "exponential".to_string(),
8917            loc: Loc {
8918                line: tok.line,
8919                column: tok.column,
8920            },
8921            leading_trivia: Vec::new(),
8922            trailing_trivia: Vec::new(),
8923        };
8924        self.consume(TokenType::LBrace)?;
8925        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8926            let field_name = self.current().value.clone();
8927            self.advance();
8928            if !self.check(TokenType::Colon) {
8929                if self.check(TokenType::LBrace) {
8930                    self.skip_braced_block()?;
8931                }
8932                continue;
8933            }
8934            self.advance();
8935            match field_name.as_str() {
8936                "watch" => node.watch = self.parse_bracketed_identifiers()?,
8937                "sensitivity" => node.sensitivity = self.parse_optional_float(),
8938                "baseline" => node.baseline = self.consume_any_ident_or_kw()?.value,
8939                "window" => {
8940                    if let Some(v) = self.parse_optional_int() {
8941                        node.window = v;
8942                    }
8943                }
8944                "scope" => {
8945                    let s_tok = self.consume_any_ident_or_kw()?;
8946                    let s = s_tok.value;
8947                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
8948                        return Err(ParseError {
8949                            message: format!(
8950                                "Invalid scope '{s}' in immune '{}' — \
8951                                 expected tenant | flow | global",
8952                                node.name
8953                            ),
8954                            line: s_tok.line,
8955                            column: s_tok.column,
8956                                                    ..Default::default()
8957                        });
8958                    }
8959                    node.scope = s;
8960                }
8961                "tau" => {
8962                    let t = self.current().clone();
8963                    match t.ttype {
8964                        TokenType::Duration | TokenType::StringLit => {
8965                            self.advance();
8966                            node.tau = t.value;
8967                        }
8968                        _ => node.tau = self.consume_any_ident_or_kw()?.value,
8969                    }
8970                }
8971                "decay" => {
8972                    let d_tok = self.consume_any_ident_or_kw()?;
8973                    let d = d_tok.value;
8974                    if !matches!(d.as_str(), "exponential" | "linear" | "none") {
8975                        return Err(ParseError {
8976                            message: format!(
8977                                "Invalid decay '{d}' in immune '{}' — \
8978                                 expected exponential | linear | none",
8979                                node.name
8980                            ),
8981                            line: d_tok.line,
8982                            column: d_tok.column,
8983                                                    ..Default::default()
8984                        });
8985                    }
8986                    node.decay = d;
8987                }
8988                _ => self.skip_value(),
8989            }
8990        }
8991        self.consume(TokenType::RBrace)?;
8992        Ok(node)
8993    }
8994
8995    /// Parse: `reflex Name { trigger, on_level, action, scope, sla }`.
8996    fn parse_reflex(&mut self) -> Result<ReflexDefinition, ParseError> {
8997        let tok = self.consume(TokenType::Reflex)?;
8998        let name = self.consume(TokenType::Identifier)?.value;
8999        let mut node = ReflexDefinition {
9000            name,
9001            trigger: String::new(),
9002            on_level: "doubt".to_string(),
9003            action: String::new(),
9004            scope: String::new(),
9005            sla: String::new(),
9006            loc: Loc {
9007                line: tok.line,
9008                column: tok.column,
9009            },
9010            leading_trivia: Vec::new(),
9011            trailing_trivia: Vec::new(),
9012        };
9013        self.consume(TokenType::LBrace)?;
9014        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9015            let field_name = self.current().value.clone();
9016            self.advance();
9017            if !self.check(TokenType::Colon) {
9018                if self.check(TokenType::LBrace) {
9019                    self.skip_braced_block()?;
9020                }
9021                continue;
9022            }
9023            self.advance();
9024            match field_name.as_str() {
9025                "trigger" => node.trigger = self.consume_any_ident_or_kw()?.value,
9026                "on_level" => {
9027                    let l_tok = self.consume_any_ident_or_kw()?;
9028                    let l = l_tok.value;
9029                    if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
9030                        return Err(ParseError {
9031                            message: format!(
9032                                "Invalid on_level '{l}' in reflex '{}' — \
9033                                 expected know | believe | speculate | doubt",
9034                                node.name
9035                            ),
9036                            line: l_tok.line,
9037                            column: l_tok.column,
9038                                                    ..Default::default()
9039                        });
9040                    }
9041                    node.on_level = l;
9042                }
9043                "action" => {
9044                    let a_tok = self.consume_any_ident_or_kw()?;
9045                    let a = a_tok.value;
9046                    if !matches!(
9047                        a.as_str(),
9048                        "drop"
9049                            | "revoke"
9050                            | "emit"
9051                            | "redact"
9052                            | "quarantine"
9053                            | "terminate"
9054                            | "alert"
9055                    ) {
9056                        return Err(ParseError {
9057                            message: format!(
9058                                "Invalid action '{a}' in reflex '{}' — \
9059                                 expected drop | revoke | emit | redact | \
9060                                 quarantine | terminate | alert",
9061                                node.name
9062                            ),
9063                            line: a_tok.line,
9064                            column: a_tok.column,
9065                                                    ..Default::default()
9066                        });
9067                    }
9068                    node.action = a;
9069                }
9070                "scope" => {
9071                    let s_tok = self.consume_any_ident_or_kw()?;
9072                    let s = s_tok.value;
9073                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
9074                        return Err(ParseError {
9075                            message: format!(
9076                                "Invalid scope '{s}' in reflex '{}' — \
9077                                 expected tenant | flow | global",
9078                                node.name
9079                            ),
9080                            line: s_tok.line,
9081                            column: s_tok.column,
9082                                                    ..Default::default()
9083                        });
9084                    }
9085                    node.scope = s;
9086                }
9087                "sla" => {
9088                    let t = self.current().clone();
9089                    match t.ttype {
9090                        TokenType::Duration | TokenType::StringLit => {
9091                            self.advance();
9092                            node.sla = t.value;
9093                        }
9094                        _ => node.sla = self.consume_any_ident_or_kw()?.value,
9095                    }
9096                }
9097                _ => self.skip_value(),
9098            }
9099        }
9100        self.consume(TokenType::RBrace)?;
9101        Ok(node)
9102    }
9103
9104    /// Parse: `heal Name { source, on_level, mode, scope, review_sla, shield, max_patches }`.
9105    fn parse_heal(&mut self) -> Result<HealDefinition, ParseError> {
9106        let tok = self.consume(TokenType::Heal)?;
9107        let name = self.consume(TokenType::Identifier)?.value;
9108        let mut node = HealDefinition {
9109            name,
9110            source: String::new(),
9111            on_level: "doubt".to_string(),
9112            mode: "human_in_loop".to_string(),
9113            scope: String::new(),
9114            review_sla: String::new(),
9115            shield_ref: String::new(),
9116            max_patches: 3,
9117            loc: Loc {
9118                line: tok.line,
9119                column: tok.column,
9120            },
9121            leading_trivia: Vec::new(),
9122            trailing_trivia: Vec::new(),
9123        };
9124        self.consume(TokenType::LBrace)?;
9125        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9126            let field_name = self.current().value.clone();
9127            self.advance();
9128            if !self.check(TokenType::Colon) {
9129                if self.check(TokenType::LBrace) {
9130                    self.skip_braced_block()?;
9131                }
9132                continue;
9133            }
9134            self.advance();
9135            match field_name.as_str() {
9136                "source" => node.source = self.consume_any_ident_or_kw()?.value,
9137                "on_level" => {
9138                    let l_tok = self.consume_any_ident_or_kw()?;
9139                    let l = l_tok.value;
9140                    if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
9141                        return Err(ParseError {
9142                            message: format!(
9143                                "Invalid on_level '{l}' in heal '{}' — \
9144                                 expected know | believe | speculate | doubt",
9145                                node.name
9146                            ),
9147                            line: l_tok.line,
9148                            column: l_tok.column,
9149                                                    ..Default::default()
9150                        });
9151                    }
9152                    node.on_level = l;
9153                }
9154                "mode" => {
9155                    let m_tok = self.consume_any_ident_or_kw()?;
9156                    let m = m_tok.value;
9157                    if !matches!(m.as_str(), "audit_only" | "human_in_loop" | "adversarial") {
9158                        return Err(ParseError {
9159                            message: format!(
9160                                "Invalid mode '{m}' in heal '{}' — \
9161                                 expected audit_only | human_in_loop | adversarial",
9162                                node.name
9163                            ),
9164                            line: m_tok.line,
9165                            column: m_tok.column,
9166                                                    ..Default::default()
9167                        });
9168                    }
9169                    node.mode = m;
9170                }
9171                "scope" => {
9172                    let s_tok = self.consume_any_ident_or_kw()?;
9173                    let s = s_tok.value;
9174                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
9175                        return Err(ParseError {
9176                            message: format!(
9177                                "Invalid scope '{s}' in heal '{}' — \
9178                                 expected tenant | flow | global",
9179                                node.name
9180                            ),
9181                            line: s_tok.line,
9182                            column: s_tok.column,
9183                                                    ..Default::default()
9184                        });
9185                    }
9186                    node.scope = s;
9187                }
9188                "review_sla" => {
9189                    let t = self.current().clone();
9190                    match t.ttype {
9191                        TokenType::Duration | TokenType::StringLit => {
9192                            self.advance();
9193                            node.review_sla = t.value;
9194                        }
9195                        _ => node.review_sla = self.consume_any_ident_or_kw()?.value,
9196                    }
9197                }
9198                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
9199                "max_patches" => {
9200                    if let Some(v) = self.parse_optional_int() {
9201                        node.max_patches = v;
9202                    }
9203                }
9204                _ => self.skip_value(),
9205            }
9206        }
9207        self.consume(TokenType::RBrace)?;
9208        Ok(node)
9209    }
9210
9211    // ── §λ-L-E Fase 9 — UI cognitiva (component / view) ────────────
9212
9213    /// Parse: `component Name { renders, via_shield, on_interact, render_hint }`.
9214    fn parse_component(&mut self) -> Result<ComponentDefinition, ParseError> {
9215        let tok = self.consume(TokenType::Component)?;
9216        let name = self.consume(TokenType::Identifier)?.value;
9217        let mut node = ComponentDefinition {
9218            name,
9219            renders: String::new(),
9220            via_shield: String::new(),
9221            on_interact: String::new(),
9222            render_hint: "custom".to_string(),
9223            loc: Loc {
9224                line: tok.line,
9225                column: tok.column,
9226            },
9227            leading_trivia: Vec::new(),
9228            trailing_trivia: Vec::new(),
9229        };
9230        self.consume(TokenType::LBrace)?;
9231        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9232            let field_name = self.current().value.clone();
9233            self.advance();
9234            if !self.check(TokenType::Colon) {
9235                if self.check(TokenType::LBrace) {
9236                    self.skip_braced_block()?;
9237                }
9238                continue;
9239            }
9240            self.advance();
9241            match field_name.as_str() {
9242                "renders" => node.renders = self.consume_any_ident_or_kw()?.value,
9243                "via_shield" => node.via_shield = self.consume_any_ident_or_kw()?.value,
9244                "on_interact" => node.on_interact = self.consume_any_ident_or_kw()?.value,
9245                "render_hint" => {
9246                    let h_tok = self.consume_any_ident_or_kw()?;
9247                    let h = h_tok.value;
9248                    if !matches!(h.as_str(), "card" | "list" | "form" | "chart" | "custom") {
9249                        return Err(ParseError {
9250                            message: format!(
9251                                "Invalid render_hint '{h}' in component '{}' — \
9252                                 expected card | list | form | chart | custom",
9253                                node.name
9254                            ),
9255                            line: h_tok.line,
9256                            column: h_tok.column,
9257                                                    ..Default::default()
9258                        });
9259                    }
9260                    node.render_hint = h;
9261                }
9262                _ => self.skip_value(),
9263            }
9264        }
9265        self.consume(TokenType::RBrace)?;
9266        Ok(node)
9267    }
9268
9269    /// Parse: `view Name { title, components: [...], route }`.
9270    fn parse_view(&mut self) -> Result<ViewDefinition, ParseError> {
9271        let tok = self.consume(TokenType::View)?;
9272        let name = self.consume(TokenType::Identifier)?.value;
9273        let mut node = ViewDefinition {
9274            name,
9275            title: String::new(),
9276            components: Vec::new(),
9277            route: String::new(),
9278            loc: Loc {
9279                line: tok.line,
9280                column: tok.column,
9281            },
9282            leading_trivia: Vec::new(),
9283            trailing_trivia: Vec::new(),
9284        };
9285        self.consume(TokenType::LBrace)?;
9286        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9287            let field_name = self.current().value.clone();
9288            self.advance();
9289            if !self.check(TokenType::Colon) {
9290                if self.check(TokenType::LBrace) {
9291                    self.skip_braced_block()?;
9292                }
9293                continue;
9294            }
9295            self.advance();
9296            match field_name.as_str() {
9297                "title" => node.title = self.consume(TokenType::StringLit)?.value,
9298                "components" => node.components = self.parse_bracketed_identifiers()?,
9299                "route" => node.route = self.consume(TokenType::StringLit)?.value,
9300                _ => self.skip_value(),
9301            }
9302        }
9303        self.consume(TokenType::RBrace)?;
9304        Ok(node)
9305    }
9306
9307    fn parse_axonendpoint(&mut self) -> Result<AxonEndpointDefinition, ParseError> {
9308        let tok = self.consume(TokenType::AxonEndpoint)?;
9309        let name = self.consume(TokenType::Identifier)?.value;
9310        let mut node = AxonEndpointDefinition {
9311            name,
9312            method: String::new(),
9313            path: String::new(),
9314            body_type: String::new(),
9315            execute_flow: String::new(),
9316            output_type: String::new(),
9317            shield_ref: String::new(),
9318            // §Fase 83.a — `cors:` reference; empty ≡ no cors declared
9319            // (D83.5: no CORS headers, ever — secure by default).
9320            cors_ref: String::new(),
9321            retries: None,
9322            timeout: String::new(),
9323            compliance: Vec::new(),
9324            // §Fase 30 — Defaults preserve backwards compat per D1.
9325            transport: "json".to_string(),
9326            keepalive: String::new(),
9327            // §Fase 31.b — Inference fields (parser-default state).
9328            // Both fields toggle/populate only when the source provides
9329            // an explicit `transport:` declaration (parser sets
9330            // `transport_explicit = true`) AND the type-checker walks
9331            // the program to compute `implicit_transport`.
9332            transport_explicit: false,
9333            implicit_transport: String::new(),
9334            // §Fase 32.g (D8) — auth scope; empty list ≡ no auth gate.
9335            requires_capabilities: Vec::new(),
9336            // §Fase 89.a — explicit authorization-coverage opt-out. Default
9337            // false; the §89.b rule requires coverage OR `public: true`.
9338            public: false,
9339            // §Fase 32.h — Replay-token binding (D9 plan-vivo).
9340            // Parser defaults: not explicit; effective value resolved
9341            // at deploy time using the method-default heuristic.
9342            replay_explicit: false,
9343            replay: false,
9344            // §Fase 33.z.k.b (v1.28.0) — Wire-format dialect default
9345            // empty; the runtime classifier resolves the default
9346            // dialect per the algebraic-effect predicate when the
9347            // source omits `transport: sse(<dialect>)`.
9348            transport_dialect: String::new(),
9349            // §Fase 33.z.k.1 (v1.27.1) — Algebraic-effect override.
9350            // Parser default false; populated by the type-checker's
9351            // compute_implicit_transports pass once the full program
9352            // is known (the predicate cross-references tool effects
9353            // declared anywhere in the program).
9354            has_algebraic_stream_effect: false,
9355            // §Fase 36.d (D2) — declared execution backend; empty ≡
9356            // not declared (the endpoint resolves down the Fase 36 D1
9357            // ladder). A non-empty value is validated against the
9358            // closed `AXONENDPOINT_BACKEND_VALUES` catalog below.
9359            backend: String::new(),
9360            // §Fase 37.y (D1) — Path-param names extracted from the
9361            // `path:` string AFTER the field is parsed. Initialized
9362            // empty; populated by `extract_path_param_names` after
9363            // the `path:` field is read in the loop below.
9364            path_params: Vec::new(),
9365            // §Fase 37.y (D2) — Inline `query: { name: Type, name: Type? }`
9366            // block. Initialized empty; populated by the `"query"` arm
9367            // in the field loop below. Closed catalog enforced at parse
9368            // time per `axonendpoint_is_valid_query_param_type`.
9369            query_params: Vec::new(),
9370            loc: Loc {
9371                line: tok.line,
9372                column: tok.column,
9373            },
9374            leading_trivia: Vec::new(),
9375            trailing_trivia: Vec::new(),
9376        };
9377        self.consume(TokenType::LBrace)?;
9378        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9379            let field_name = self.current().value.clone();
9380            self.advance();
9381            if self.check(TokenType::Colon) {
9382                self.advance();
9383                match field_name.as_str() {
9384                    "method" => {
9385                        // §Fase 32.b D3 — closed method enum
9386                        // `{GET, POST, PUT, DELETE, PATCH}`. Unknown
9387                        // values rejected at parse time with smart-
9388                        // suggest hint (Fase 28.e). HEAD/OPTIONS/etc.
9389                        // are runtime-managed and not adopter-
9390                        // declarable.
9391                        let value_tok = self.consume_any_ident_or_kw()?;
9392                        let value_upper = value_tok.value.to_uppercase();
9393                        if !axonendpoint_is_valid_method(&value_upper) {
9394                            let hint = crate::smart_suggest::suggest_for(
9395                                &value_upper,
9396                                AXONENDPOINT_METHOD_VALUES,
9397                            );
9398                            let base = format!(
9399                                "Invalid method '{}' in axonendpoint '{}'.",
9400                                value_tok.value, node.name
9401                            );
9402                            let message = if hint.is_empty() {
9403                                format!(
9404                                    "{base} expected GET | POST | PUT | DELETE | PATCH, found {}",
9405                                    value_tok.value
9406                                )
9407                            } else {
9408                                format!(
9409                                    "{base} {hint} (expected GET | POST | PUT | DELETE | PATCH, found {})",
9410                                    value_tok.value
9411                                )
9412                            };
9413                            return Err(ParseError {
9414                                message,
9415                                line: value_tok.line,
9416                                column: value_tok.column,
9417                                ..Default::default()
9418                            });
9419                        }
9420                        node.method = value_upper;
9421                    }
9422                    "path" => {
9423                        node.path = self.consume(TokenType::StringLit)?.value.clone();
9424                        // §Fase 37.y (D1) — extract `{name}` placeholders
9425                        // for the Request Binding Contract's path-param
9426                        // source. Duplicate `{name}` in the same path
9427                        // is rejected at parse time (HTTP route patterns
9428                        // structurally reject duplicates; surfacing the
9429                        // error here is friendlier than letting axum
9430                        // panic at registration).
9431                        match extract_path_param_names(&node.path) {
9432                            Ok(names) => node.path_params = names,
9433                            Err(dup) => {
9434                                let cur = self.current().clone();
9435                                return Err(ParseError {
9436                                    message: format!(
9437                                        "axonendpoint '{}' declares path '{}' \
9438                                         containing duplicate placeholder '{{{}}}'. \
9439                                         Each `{{name}}` in a `path:` must be \
9440                                         unique — the runtime cannot bind two \
9441                                         path segments to the same name (Fase 37.y D1).",
9442                                        node.name, node.path, dup,
9443                                    ),
9444                                    line: cur.line,
9445                                    column: cur.column,
9446                                    ..Default::default()
9447                                });
9448                            }
9449                        }
9450                    },
9451                    "body" => node.body_type = self.consume_any_ident_or_kw()?.value.clone(),
9452                    "query" => {
9453                        // §Fase 37.y (D2) — Inline query-parameter block.
9454                        // Grammar: `query: { name: Type [, name: Type?]* }`.
9455                        // Closed type catalog
9456                        // `AXONENDPOINT_QUERY_PARAM_TYPES = {Text, Int,
9457                        // Float, Bool, Uuid}`. Optional via `?` suffix
9458                        // reuses `TypeExpr.optional` semantics already in
9459                        // use for flow parameters + body type fields. A
9460                        // duplicate field name in the same block is a
9461                        // parse error (HTTP query strings DO allow
9462                        // multi-value but v1.38.5 binds the first value
9463                        // only — see plan vivo §7 forward-compat).
9464                        //
9465                        // §Fase 37.y (D2 robustness) — declaring `query:`
9466                        // twice on the same axonendpoint silently merged
9467                        // params pre-hardening. Now it's a parse error
9468                        // so an adopter typo / copy-paste mistake
9469                        // surfaces with line + column instead of
9470                        // producing an unexpectedly-augmented endpoint.
9471                        let lbrace_tok = self.consume(TokenType::LBrace)?;
9472                        let block_line = lbrace_tok.line;
9473                        if !node.query_params.is_empty() {
9474                            return Err(ParseError {
9475                                message: format!(
9476                                    "axonendpoint '{}' declares `query: {{ … }}` \
9477                                     more than once. The query-parameter block \
9478                                     is unique per endpoint; combine all params \
9479                                     into a single block (Fase 37.y D2).",
9480                                    node.name,
9481                                ),
9482                                line: lbrace_tok.line,
9483                                column: lbrace_tok.column,
9484                                ..Default::default()
9485                            });
9486                        }
9487                        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9488                            let name_tok = self.consume(TokenType::Identifier)?;
9489                            let field_name = name_tok.value.clone();
9490                            // Duplicate detection within the block.
9491                            if node
9492                                .query_params
9493                                .iter()
9494                                .any(|f| f.name == field_name)
9495                            {
9496                                return Err(ParseError {
9497                                    message: format!(
9498                                        "axonendpoint '{}' declares duplicate \
9499                                         query param '{}' inside `query: {{ … }}`. \
9500                                         Each name must appear at most once \
9501                                         (Fase 37.y D2).",
9502                                        node.name, field_name,
9503                                    ),
9504                                    line: name_tok.line,
9505                                    column: name_tok.column,
9506                                    ..Default::default()
9507                                });
9508                            }
9509                            self.consume(TokenType::Colon)?;
9510                            let type_expr = self.parse_type_expr()?;
9511                            // §Fase 37.y (D2 robustness) — reject generic
9512                            // type expressions on query params. The
9513                            // closed catalog is 5 primitives; container
9514                            // types (`Optional<T>`, `List<T>`, etc.)
9515                            // would mislead the adopter into thinking
9516                            // they bind multi-value query strings
9517                            // (deferred per plan vivo §7) or that
9518                            // `Optional<Text>` is the canonical way to
9519                            // declare an optional query (it's NOT —
9520                            // `Text?` is). Surface the canonical syntax
9521                            // verbatim so the fix is obvious.
9522                            if !type_expr.generic_param.is_empty() {
9523                                let canonical_hint = if type_expr.name == "Optional" {
9524                                    format!(
9525                                        " Use `{}?` (the `?` suffix) for an \
9526                                         optional query param instead of \
9527                                         `Optional<{}>`.",
9528                                        type_expr.generic_param,
9529                                        type_expr.generic_param,
9530                                    )
9531                                } else if type_expr.name == "List" {
9532                                    " Multi-value query params (e.g. `?tag=a&tag=b`) \
9533                                     are honest-deferred from v1.38.5; bind a \
9534                                     single-value `Text` query param and parse \
9535                                     the value inside the flow."
9536                                        .to_string()
9537                                } else {
9538                                    String::new()
9539                                };
9540                                return Err(ParseError {
9541                                    message: format!(
9542                                        "axonendpoint '{}' query param '{}' uses \
9543                                         a generic type `{}<{}>`. Query params \
9544                                         take a primitive type from the closed \
9545                                         catalog ({}); the `?` suffix marks \
9546                                         optional.{} (Fase 37.y D2).",
9547                                        node.name,
9548                                        field_name,
9549                                        type_expr.name,
9550                                        type_expr.generic_param,
9551                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | "),
9552                                        canonical_hint,
9553                                    ),
9554                                    line: type_expr.loc.line,
9555                                    column: type_expr.loc.column,
9556                                    ..Default::default()
9557                                });
9558                            }
9559                            // Validate against the closed catalog. A
9560                            // miss surfaces a Fase 28-style smart-suggest
9561                            // hint when within edit-distance 2.
9562                            if !axonendpoint_is_valid_query_param_type(&type_expr.name) {
9563                                // `smart_suggest::suggest_for` returns
9564                                // pre-formatted prose like
9565                                // "Did you mean `Text`?" or
9566                                // "Did you mean `Text` or `Int`?" (empty
9567                                // when no candidate within edit-distance
9568                                // 2). Concatenate without re-wrapping.
9569                                let hint = crate::smart_suggest::suggest_for(
9570                                    &type_expr.name,
9571                                    AXONENDPOINT_QUERY_PARAM_TYPES,
9572                                );
9573                                let hint_text = if hint.is_empty() {
9574                                    format!(
9575                                        " Expected one of: {}.",
9576                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | ")
9577                                    )
9578                                } else {
9579                                    format!(
9580                                        " {} Expected one of: {}.",
9581                                        hint,
9582                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | ")
9583                                    )
9584                                };
9585                                return Err(ParseError {
9586                                    message: format!(
9587                                        "axonendpoint '{}' query param '{}' has \
9588                                         unsupported type '{}'.{} (Fase 37.y D2).",
9589                                        node.name, field_name, type_expr.name,
9590                                        hint_text,
9591                                    ),
9592                                    line: type_expr.loc.line,
9593                                    column: type_expr.loc.column,
9594                                    ..Default::default()
9595                                });
9596                            }
9597                            node.query_params.push(TypeField {
9598                                name: field_name,
9599                                type_expr,
9600                                loc: Loc {
9601                                    line: name_tok.line,
9602                                    column: name_tok.column,
9603                                },
9604                            });
9605                            // Trailing comma is optional; the next loop
9606                            // iteration handles `}` cleanly. Accept both
9607                            // `name: Type, name: Type` AND `name: Type
9608                            // name: Type` (the existing parser style is
9609                            // forgiving about list separators).
9610                            if self.check(TokenType::Comma) {
9611                                self.advance();
9612                            }
9613                            let _ = block_line; // suppress unused warning
9614                        }
9615                        self.consume(TokenType::RBrace)?;
9616                    },
9617                    "execute" => node.execute_flow = self.consume_any_ident_or_kw()?.value.clone(),
9618                    "output" => {
9619                        // §Fase 38.x.f — promote axonendpoint `output:`
9620                        // parsing from a single token to the full
9621                        // generic-aware type expression (mirroring
9622                        // `parse_step` for FlowStep::Step which already
9623                        // uses `parse_output_type_string`).
9624                        //
9625                        // Pre-38.x.f: `output: List<Item>` captured only
9626                        // `"List"`, dropping `<Item>` (next tokens were
9627                        // either left unconsumed or absorbed by the
9628                        // following field). v1.39.0's narrow cardinality
9629                        // gate happened to fire correctly for `output: T`
9630                        // + retrieve-tail because the singular-detection
9631                        // path used `!starts_with("List<")` — but the
9632                        // SYMMETRIC `output: List<T>` + singular-tail
9633                        // case (38.x.f D3) needs the FULL `List<T>`
9634                        // shape captured; without it the gate sees
9635                        // `"List"` and misclassifies as Singular.
9636                        node.output_type = self.parse_output_type_string()?;
9637                    }
9638                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
9639                    // §Fase 83.a — the `cors: <Name>` reference.
9640                    "cors" => node.cors_ref = self.consume_any_ident_or_kw()?.value.clone(),
9641                    "retries" => node.retries = self.parse_optional_int(),
9642                    "timeout" => {
9643                        let t = self.current().clone();
9644                        self.advance();
9645                        node.timeout = t.value.clone();
9646                    }
9647                    "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
9648                    "replay" => {
9649                        // §Fase 32.h (D9 plan-vivo) — Replay-token binding.
9650                        // Boolean `replay: true | false`. Default (when
9651                        // omitted) is method-derived at deploy-time:
9652                        // POST/PUT → true, GET/DELETE → false. Explicit
9653                        // declaration sets `replay_explicit = true` so
9654                        // the runtime knows NOT to override.
9655                        let value_tok = self.consume(TokenType::Bool)?;
9656                        node.replay = value_tok.value.eq_ignore_ascii_case("true");
9657                        node.replay_explicit = true;
9658                    }
9659                    // §Fase 89.a — `public: true | false`, the explicit
9660                    // authorization-coverage opt-out (doctrine
9661                    // `every_boundary_is_guarded`). Mirrors `replay:`'s bool
9662                    // parse. Default false; the §89.b rule (`axon-T890`)
9663                    // requires a covering discipline OR `public: true`.
9664                    "public" => {
9665                        let value_tok = self.consume(TokenType::Bool)?;
9666                        node.public = value_tok.value.eq_ignore_ascii_case("true");
9667                    }
9668                    "requires" => {
9669                        // §Fase 32.g (D8) — Auth scope per axonendpoint.
9670                        // Closed slug grammar
9671                        // `^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$` enforced
9672                        // at parse time with smart-suggest-style hint.
9673                        // Empty list means "no auth gate" (D9 backwards-
9674                        // compat). Cross-stack with Python parser.
9675                        let bracket_tok = self.current().clone();
9676                        let items = self.parse_bracketed_dot_identifiers()?;
9677                        for slug in &items {
9678                            if !is_valid_capability_slug(slug) {
9679                                return Err(ParseError {
9680                                    message: format!(
9681                                        "Invalid capability slug '{slug}' in axonendpoint '{}' \
9682                                         `requires:`. Capability slugs must match \
9683                                         ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
9684                                         lowercase identifiers starting with a letter. Examples: \
9685                                         `admin`, `legal.read`, `hipaa.phi.read`.",
9686                                        node.name
9687                                    ),
9688                                    line: bracket_tok.line,
9689                                    column: bracket_tok.column,
9690                                    ..Default::default()
9691                                });
9692                            }
9693                        }
9694                        node.requires_capabilities = items;
9695                    }
9696                    // §Fase 30.b — HTTP transport enum (D2 closed) + keepalive (D6 closed).
9697                    // Mirrors `axon/compiler/parser.py` `_parse_axonendpoint`.
9698                    // Drift-gate corpus verifies byte-identical parse cross-stack.
9699                    "transport" => {
9700                        let value_tok = self.consume_any_ident_or_kw()?;
9701                        let value = &value_tok.value;
9702                        if !axonendpoint_is_valid_transport(value) {
9703                            let hint = crate::smart_suggest::suggest_for(
9704                                value,
9705                                AXONENDPOINT_TRANSPORT_VALUES,
9706                            );
9707                            let base = format!(
9708                                "Invalid transport '{}' in axonendpoint '{}'.",
9709                                value, node.name
9710                            );
9711                            let message = if hint.is_empty() {
9712                                format!("{base} expected json | sse | ndjson, found {value}")
9713                            } else {
9714                                format!(
9715                                    "{base} {hint} (expected json | sse | ndjson, found {value})"
9716                                )
9717                            };
9718                            return Err(ParseError {
9719                                message,
9720                                line: value_tok.line,
9721                                column: value_tok.column,
9722                                ..Default::default()
9723                            });
9724                        }
9725                        node.transport = value.clone();
9726                        // §Fase 31.b D1 — mark the field as explicitly
9727                        // declared so the type-checker's implicit-transport
9728                        // inference knows NOT to override this value with
9729                        // the produces_stream-driven inference.
9730                        node.transport_explicit = true;
9731                        // §Fase 33.z.k.b (v1.28.0) — Optional dialect
9732                        // parametrization: `transport: sse(<dialect>)`.
9733                        // Only valid when the base value is `sse`
9734                        // (json + ndjson dialects are the dialects
9735                        // themselves; `json(<x>)` / `ndjson(<x>)`
9736                        // would be parse errors caught below).
9737                        if self.check(TokenType::LParen) {
9738                            if value != "sse" {
9739                                let tok = self.current().clone();
9740                                return Err(ParseError {
9741                                    message: format!(
9742                                        "Dialect parametrization \
9743                                         `transport: {value}(<dialect>)` is \
9744                                         only valid for `sse`; got \
9745                                         `{value}` in axonendpoint '{}'.",
9746                                        node.name
9747                                    ),
9748                                    line: tok.line,
9749                                    column: tok.column,
9750                                    ..Default::default()
9751                                });
9752                            }
9753                            self.advance(); // consume LParen
9754                            let dialect_tok = self.consume_any_ident_or_kw()?;
9755                            let dialect = dialect_tok.value.clone();
9756                            if !AXONENDPOINT_TRANSPORT_DIALECTS
9757                                .iter()
9758                                .any(|&d| d == dialect)
9759                            {
9760                                let hint = crate::smart_suggest::suggest_for(
9761                                    &dialect,
9762                                    AXONENDPOINT_TRANSPORT_DIALECTS,
9763                                );
9764                                let base = format!(
9765                                    "Invalid SSE dialect '{dialect}' in axonendpoint '{}'.",
9766                                    node.name
9767                                );
9768                                let message = if hint.is_empty() {
9769                                    format!(
9770                                        "{base} expected axon | openai | kimi | glm | anthropic, found {dialect}"
9771                                    )
9772                                } else {
9773                                    format!(
9774                                        "{base} {hint} (expected axon | openai | kimi | glm | anthropic, found {dialect})"
9775                                    )
9776                                };
9777                                return Err(ParseError {
9778                                    message,
9779                                    line: dialect_tok.line,
9780                                    column: dialect_tok.column,
9781                                    ..Default::default()
9782                                });
9783                            }
9784                            // Closing RParen.
9785                            let rparen_tok = self.current().clone();
9786                            if !self.check(TokenType::RParen) {
9787                                return Err(ParseError {
9788                                    message: format!(
9789                                        "Expected `)` after dialect name \
9790                                         in axonendpoint '{}' \
9791                                         (transport: sse(<dialect>) grammar).",
9792                                        node.name
9793                                    ),
9794                                    line: rparen_tok.line,
9795                                    column: rparen_tok.column,
9796                                    ..Default::default()
9797                                });
9798                            }
9799                            self.advance(); // consume RParen
9800                            node.transport_dialect = dialect;
9801                        }
9802                    }
9803                    "keepalive" => {
9804                        // Accepts either a DURATION token (e.g. `15s`) or
9805                        // an ident-like token. Validation against the
9806                        // closed enum {5s, 15s, 30s, 60s} happens after.
9807                        let value_tok = self.current().clone();
9808                        self.advance();
9809                        let value = &value_tok.value;
9810                        if !axonendpoint_is_valid_keepalive(value) {
9811                            let hint = crate::smart_suggest::suggest_for(
9812                                value,
9813                                AXONENDPOINT_KEEPALIVE_VALUES,
9814                            );
9815                            let base = format!(
9816                                "Invalid keepalive '{}' in axonendpoint '{}'.",
9817                                value, node.name
9818                            );
9819                            let message = if hint.is_empty() {
9820                                format!("{base} expected 5s | 15s | 30s | 60s, found {value}")
9821                            } else {
9822                                format!(
9823                                    "{base} {hint} (expected 5s | 15s | 30s | 60s, found {value})"
9824                                )
9825                            };
9826                            return Err(ParseError {
9827                                message,
9828                                line: value_tok.line,
9829                                column: value_tok.column,
9830                                ..Default::default()
9831                            });
9832                        }
9833                        node.keepalive = value.clone();
9834                    }
9835                    "backend" => {
9836                        // §Fase 36.d (D2) — declared execution backend.
9837                        // Closed catalog `CANONICAL_PROVIDERS ∪ {auto,
9838                        // stub}`; an unknown name is a parse error with
9839                        // a smart-suggest hint (the same discipline as
9840                        // `method`/`transport`/`keepalive`). The
9841                        // type-checker re-validates defensively for
9842                        // ASTs built outside the parser (LSP, tests).
9843                        let value_tok = self.consume_any_ident_or_kw()?;
9844                        let value = &value_tok.value;
9845                        if !axonendpoint_is_valid_backend(value) {
9846                            let hint = crate::smart_suggest::suggest_for(
9847                                value,
9848                                AXONENDPOINT_BACKEND_VALUES,
9849                            );
9850                            let expected = AXONENDPOINT_BACKEND_VALUES.join(" | ");
9851                            let base = format!(
9852                                "Invalid backend '{}' in axonendpoint '{}'.",
9853                                value, node.name
9854                            );
9855                            let message = if hint.is_empty() {
9856                                format!("{base} expected {expected}, found {value}")
9857                            } else {
9858                                format!(
9859                                    "{base} {hint} (expected {expected}, found {value})"
9860                                )
9861                            };
9862                            return Err(ParseError {
9863                                message,
9864                                line: value_tok.line,
9865                                column: value_tok.column,
9866                                ..Default::default()
9867                            });
9868                        }
9869                        node.backend = value.clone();
9870                    }
9871                    _ => self.skip_value(),
9872                }
9873            } else if self.check(TokenType::LBrace) {
9874                self.skip_braced_block()?;
9875            }
9876        }
9877        self.consume(TokenType::RBrace)?;
9878        Ok(node)
9879    }
9880
9881    // ── Numeric helpers for Tier 2 field parsing ────────────────────
9882
9883    fn parse_optional_int(&mut self) -> Option<i64> {
9884        let tok = self.current().clone();
9885        match tok.ttype {
9886            TokenType::Integer => {
9887                self.advance();
9888                tok.value.parse::<i64>().ok()
9889            }
9890            _ => {
9891                self.advance();
9892                None
9893            }
9894        }
9895    }
9896
9897    fn parse_optional_float(&mut self) -> Option<f64> {
9898        let tok = self.current().clone();
9899        match tok.ttype {
9900            TokenType::Float | TokenType::Integer => {
9901                self.advance();
9902                tok.value.parse::<f64>().ok()
9903            }
9904            _ => {
9905                self.advance();
9906                None
9907            }
9908        }
9909    }
9910
9911    // ── LAMBDA DATA (ΛD) ──────────────────────────────────────────
9912
9913    fn parse_lambda_data(&mut self) -> Result<LambdaDataDefinition, ParseError> {
9914        let tok = self.consume(TokenType::Lambda)?;
9915        let name = self.consume(TokenType::Identifier)?;
9916        self.consume(TokenType::LBrace)?;
9917
9918        let mut node = LambdaDataDefinition {
9919            name: name.value.clone(),
9920            ontology: String::new(),
9921            certainty: 1.0,
9922            temporal_frame_start: String::new(),
9923            temporal_frame_end: String::new(),
9924            provenance: String::new(),
9925            derivation: String::new(),
9926            loc: Loc {
9927                line: tok.line,
9928                column: tok.column,
9929            },
9930            leading_trivia: Vec::new(),
9931            trailing_trivia: Vec::new(),
9932        };
9933
9934        while !self.check(TokenType::RBrace) {
9935            let field = self.current().clone();
9936            match field.ttype {
9937                TokenType::Ontology => {
9938                    self.advance();
9939                    self.consume(TokenType::Colon)?;
9940                    node.ontology = self.consume(TokenType::StringLit)?.value.clone();
9941                }
9942                TokenType::Certainty => {
9943                    self.advance();
9944                    self.consume(TokenType::Colon)?;
9945                    let val = self.current().clone();
9946                    match val.ttype {
9947                        TokenType::Float => {
9948                            self.advance();
9949                            node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
9950                        }
9951                        TokenType::Integer => {
9952                            self.advance();
9953                            node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
9954                        }
9955                        _ => {
9956                            return Err(ParseError {
9957                                message: format!(
9958                                    "Expected number for certainty, got '{}'",
9959                                    val.value
9960                                ),
9961                                line: val.line,
9962                                column: val.column,
9963                                                            ..Default::default()
9964                            });
9965                        }
9966                    }
9967                }
9968                TokenType::TemporalFrame => {
9969                    self.advance();
9970                    self.consume(TokenType::Colon)?;
9971                    node.temporal_frame_start = self.consume(TokenType::StringLit)?.value.clone();
9972                    // Optional second string for end frame
9973                    if self.check(TokenType::StringLit) {
9974                        node.temporal_frame_end = self.consume(TokenType::StringLit)?.value.clone();
9975                    }
9976                }
9977                TokenType::Provenance => {
9978                    self.advance();
9979                    self.consume(TokenType::Colon)?;
9980                    node.provenance = self.consume(TokenType::StringLit)?.value.clone();
9981                }
9982                TokenType::Derivation => {
9983                    self.advance();
9984                    self.consume(TokenType::Colon)?;
9985                    let d = self.current().clone();
9986                    self.advance();
9987                    node.derivation = d.value.clone();
9988                }
9989                _ => {
9990                    // Skip unknown fields gracefully
9991                    self.advance();
9992                    if self.check(TokenType::Colon) {
9993                        self.advance();
9994                        self.skip_value();
9995                    }
9996                }
9997            }
9998        }
9999
10000        self.consume(TokenType::RBrace)?;
10001        Ok(node)
10002    }
10003
10004    fn parse_lambda_data_apply(&mut self) -> Result<LambdaDataApplyNode, ParseError> {
10005        let tok = self.consume(TokenType::Lambda)?;
10006        let lambda_name = self.consume(TokenType::Identifier)?;
10007
10008        // Expect "on" keyword (parsed as identifier since it's not reserved)
10009        let on_tok = self.current().clone();
10010        self.advance();
10011        if on_tok.value != "on" {
10012            return Err(ParseError {
10013                message: format!(
10014                    "Expected 'on' after lambda data name in flow step, got '{}'",
10015                    on_tok.value
10016                ),
10017                line: on_tok.line,
10018                column: on_tok.column,
10019                            ..Default::default()
10020            });
10021        }
10022
10023        let target = self.current().clone();
10024        self.advance();
10025
10026        let mut output_type = String::new();
10027        if self.check(TokenType::Arrow) {
10028            self.advance();
10029            output_type = self.consume(TokenType::Identifier)?.value.clone();
10030        }
10031
10032        Ok(LambdaDataApplyNode {
10033            lambda_data_name: lambda_name.value.clone(),
10034            target: target.value.clone(),
10035            output_type,
10036            loc: Loc {
10037                line: tok.line,
10038                column: tok.column,
10039            },
10040        })
10041    }
10042
10043    // ── GENERIC (Tier 2+) ────────────────────────────────────────
10044
10045    fn parse_generic_declaration(&mut self) -> Result<Declaration, ParseError> {
10046        let kw_tok = self.current().clone();
10047        self.advance(); // consume keyword
10048
10049        // Try to consume a name (identifier or keyword-as-name)
10050        let name = if self.current().ttype == TokenType::Identifier {
10051            let n = self.current().value.clone();
10052            self.advance();
10053            n
10054        } else if !self.check(TokenType::LBrace)
10055            && !self.check(TokenType::LParen)
10056            && !self.check(TokenType::Eof)
10057            && self
10058                .current()
10059                .value
10060                .chars()
10061                .all(|c| c.is_alphanumeric() || c == '_')
10062        {
10063            let n = self.current().value.clone();
10064            self.advance();
10065            n
10066        } else {
10067            String::new()
10068        };
10069
10070        // Skip optional parens: (...)
10071        if self.check(TokenType::LParen) {
10072            self.advance();
10073            let mut depth = 1u32;
10074            while depth > 0 && !self.check(TokenType::Eof) {
10075                if self.check(TokenType::LParen) {
10076                    depth += 1;
10077                } else if self.check(TokenType::RParen) {
10078                    depth -= 1;
10079                }
10080                self.advance();
10081            }
10082        }
10083
10084        // Skip tokens until LBrace or next declaration
10085        while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
10086            if self.check(TokenType::Eof) {
10087                break;
10088            }
10089            self.advance();
10090        }
10091
10092        // Skip braced block if present
10093        if self.check(TokenType::LBrace) {
10094            self.skip_braced_block()?;
10095        }
10096
10097        Ok(Declaration::Generic(GenericDeclaration {
10098            keyword: kw_tok.value,
10099            name,
10100            loc: Loc {
10101                line: kw_tok.line,
10102                column: kw_tok.column,
10103            },
10104            leading_trivia: Vec::new(),
10105            trailing_trivia: Vec::new(),
10106        }))
10107    }
10108
10109    // ──────────────────────────────────────────────────────────────────
10110    //  §λ-L-E Fase 13 — Mobile Typed Channels parsers
10111    //  (paper_mobile_channels.md §3 + plan/fase_13)
10112    //  Direct port of axon/compiler/parser.py:_parse_channel/emit/publish/discover.
10113    // ──────────────────────────────────────────────────────────────────
10114
10115    /// Parse: `channel Name { message, qos, lifetime, persistence, shield }`.
10116    fn parse_channel(&mut self) -> Result<ChannelDefinition, ParseError> {
10117        let tok = self.consume(TokenType::Channel)?;
10118        let name = self.consume(TokenType::Identifier)?.value;
10119        let mut node = ChannelDefinition {
10120            name: name.clone(),
10121            message: String::new(),
10122            qos: "at_least_once".to_string(),
10123            lifetime: "affine".to_string(),
10124            persistence: "ephemeral".to_string(),
10125            shield_ref: String::new(),
10126            loc: Loc {
10127                line: tok.line,
10128                column: tok.column,
10129            },
10130            leading_trivia: Vec::new(),
10131            trailing_trivia: Vec::new(),
10132        };
10133        self.consume(TokenType::LBrace)?;
10134        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10135            let field_tok = self.current().clone();
10136            let field_name = field_tok.value.clone();
10137            self.advance();
10138            if !self.check(TokenType::Colon) {
10139                if self.check(TokenType::LBrace) {
10140                    self.skip_braced_block()?;
10141                }
10142                continue;
10143            }
10144            self.advance();
10145            match field_name.as_str() {
10146                "message" => node.message = self.parse_channel_message_type()?,
10147                "qos" => {
10148                    let q_tok = self.consume_any_ident_or_kw()?;
10149                    if !matches!(
10150                        q_tok.value.as_str(),
10151                        "at_most_once" | "at_least_once" | "exactly_once" | "broadcast" | "queue"
10152                    ) {
10153                        return Err(ParseError {
10154                            message: format!(
10155                                "Invalid qos '{}' in channel '{}' — \
10156                                 expected at_most_once | at_least_once | \
10157                                 exactly_once | broadcast | queue",
10158                                q_tok.value, name
10159                            ),
10160                            line: q_tok.line,
10161                            column: q_tok.column,
10162                                                    ..Default::default()
10163                        });
10164                    }
10165                    node.qos = q_tok.value;
10166                }
10167                "lifetime" => {
10168                    let lt_tok = self.consume_any_ident_or_kw()?;
10169                    if !matches!(lt_tok.value.as_str(), "linear" | "affine" | "persistent") {
10170                        return Err(ParseError {
10171                            message: format!(
10172                                "Invalid lifetime '{}' in channel '{}' — \
10173                                 expected linear | affine | persistent",
10174                                lt_tok.value, name
10175                            ),
10176                            line: lt_tok.line,
10177                            column: lt_tok.column,
10178                                                    ..Default::default()
10179                        });
10180                    }
10181                    node.lifetime = lt_tok.value;
10182                }
10183                "persistence" => {
10184                    let p_tok = self.consume_any_ident_or_kw()?;
10185                    if !matches!(p_tok.value.as_str(), "ephemeral" | "persistent_axonstore") {
10186                        return Err(ParseError {
10187                            message: format!(
10188                                "Invalid persistence '{}' in channel '{}' — \
10189                                 expected ephemeral | persistent_axonstore",
10190                                p_tok.value, name
10191                            ),
10192                            line: p_tok.line,
10193                            column: p_tok.column,
10194                                                    ..Default::default()
10195                        });
10196                    }
10197                    node.persistence = p_tok.value;
10198                }
10199                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
10200                _ => self.skip_value(),
10201            }
10202        }
10203        self.consume(TokenType::RBrace)?;
10204        Ok(node)
10205    }
10206
10207    /// Parse a `message:` value, supporting nested `Channel<…>`
10208    /// (second-order session types — paper §3.3).
10209    fn parse_channel_message_type(&mut self) -> Result<String, ParseError> {
10210        let head = self.consume(TokenType::Identifier)?;
10211        let mut spelling = head.value;
10212        if self.check(TokenType::Lt) {
10213            self.advance();
10214            let inner = self.parse_channel_message_type()?;
10215            self.consume(TokenType::Gt)?;
10216            spelling = format!("{}<{}>", spelling, inner);
10217        }
10218        Ok(spelling)
10219    }
10220
10221    /// Parse: `emit ChannelName(value_ref)` — Chan-Output / Chan-Mobility.
10222    ///
10223    /// `value_ref` accepts a bare identifier (variable / channel name for
10224    /// mobility) or a dotted path (`Step.output.field`) referencing a prior
10225    /// step result (Fase 13.i — runtime resolves via ContextManager).
10226    fn parse_emit_step(&mut self) -> Result<FlowStep, ParseError> {
10227        let tok = self.consume(TokenType::Emit)?;
10228        let channel = self.consume(TokenType::Identifier)?.value;
10229        self.consume(TokenType::LParen)?;
10230        let value = self.parse_emit_value_ref()?;
10231        self.consume(TokenType::RParen)?;
10232        Ok(FlowStep::Emit(EmitStatement {
10233            channel_ref: channel,
10234            value_ref: value,
10235            loc: Loc {
10236                line: tok.line,
10237                column: tok.column,
10238            },
10239        }))
10240    }
10241
10242    /// §Fase 92.b — parse `mint <Credential> as <binding>`. The credential
10243    /// reference must resolve to a declared `credential` (`axon-T895`,
10244    /// type-checker); the binding is a fresh flow-scoped name receiving the
10245    /// raw bearer string. Both tokens are required — a `mint` with no
10246    /// binding would mint authority into the void.
10247    fn parse_mint_step(&mut self) -> Result<FlowStep, ParseError> {
10248        let tok = self.consume(TokenType::Mint)?;
10249        let credential_ref = self.consume(TokenType::Identifier)?.value;
10250        self.consume(TokenType::As)?;
10251        let binding = self.consume(TokenType::Identifier)?.value;
10252        Ok(FlowStep::Mint(MintStep {
10253            credential_ref,
10254            binding,
10255            loc: Loc {
10256                line: tok.line,
10257                column: tok.column,
10258            },
10259        }))
10260    }
10261
10262    /// §Fase 94.b — parse `rotate <SecretsStore> [where "<filter>"] with
10263    /// <Tool> as <binding>` (doctrine `rotation_without_revelation`).
10264    ///
10265    /// All three anchors are grammar, not convention: the store names WHAT
10266    /// may rotate (a `backend: secrets` class view — `axon-T898` in the
10267    /// type-checker), the tool names WHO performs the exchange
10268    /// (`axon-T899`), and the binding receives the metadata-only summary —
10269    /// a `rotate` without a binding would renew authority with no
10270    /// observable outcome, so `as` is REQUIRED (the `mint` posture). The
10271    /// `where` filter is optional (§67 string grammar, proven against the
10272    /// synthesized metadata schema); omitting it rotates the WHOLE class —
10273    /// the deliberate post-breach bulk shape. `with` is a soft keyword
10274    /// (not a lexer token): reserving it globally would break every
10275    /// adopter identifier named `with`.
10276    fn parse_rotate_step(&mut self) -> Result<FlowStep, ParseError> {
10277        let tok = self.consume(TokenType::Rotate)?;
10278        let store_ref = self.consume(TokenType::Identifier)?.value;
10279        let mut where_expr = String::new();
10280        if self.check(TokenType::Where) {
10281            self.advance();
10282            where_expr = self.consume(TokenType::StringLit)?.value.clone();
10283        }
10284        let with_tok = self.current().clone();
10285        if with_tok.value != "with" {
10286            return Err(ParseError {
10287                message: format!(
10288                    "Expected `with <Tool>` after `rotate {store_ref}{}`, found '{}'. \
10289                     A rotation names the tool that performs the renewal exchange: \
10290                     `rotate {store_ref} [where \"<filter>\"] with <Tool> as <binding>`.",
10291                    if where_expr.is_empty() { "" } else { " where …" },
10292                    with_tok.value
10293                ),
10294                line: with_tok.line,
10295                column: with_tok.column,
10296                ..Default::default()
10297            });
10298        }
10299        self.advance();
10300        let tool_ref = self.consume(TokenType::Identifier)?.value;
10301        self.consume(TokenType::As)?;
10302        let binding = self.consume(TokenType::Identifier)?.value;
10303        Ok(FlowStep::Rotate(RotateStep {
10304            store_ref,
10305            where_expr,
10306            tool_ref,
10307            binding,
10308            loc: Loc {
10309                line: tok.line,
10310                column: tok.column,
10311            },
10312        }))
10313    }
10314
10315    /// Parse: `IDENTIFIER ('.' (IDENTIFIER | keyword))*` → dot-joined string
10316    /// (Fase 13.i).
10317    ///
10318    /// Mirrors the Python `_parse_emit_value_ref` helper exactly so the IR
10319    /// JSON for `emit Hello(Build.output)` is byte-identical between the
10320    /// two reference implementations.
10321    ///
10322    /// The HEAD must be a real ``Identifier``. Subsequent segments after a
10323    /// `.` may be identifiers OR keywords — common field names like
10324    /// ``output``, ``result``, ``message``, ``state``, etc. are reserved
10325    /// words in Axon but adopters must be able to write them as
10326    /// dotted-access segments. The accepting predicate:
10327    ///   - the lexer carried a non-empty `value` (every Word-like token does)
10328    ///   - the value's first byte is a letter or underscore (filters out
10329    ///     punctuation tokens such as ',', '{', etc.)
10330    fn parse_emit_value_ref(&mut self) -> Result<String, ParseError> {
10331        let head = self.consume(TokenType::Identifier)?.value;
10332        let mut parts = vec![head];
10333        while self.check(TokenType::Dot) {
10334            self.advance(); // consume '.'
10335            let next_tok = self.current().clone();
10336            let valid = !next_tok.value.is_empty()
10337                && next_tok.value.as_bytes()[0].is_ascii_alphabetic()
10338                || next_tok.value.starts_with('_');
10339            if !valid {
10340                return Err(ParseError {
10341                    message: format!(
10342                        "Expected identifier or keyword after '.' in dotted \
10343                         access, found {:?}",
10344                        next_tok.value
10345                    ),
10346                    line: next_tok.line,
10347                    column: next_tok.column,
10348                                    ..Default::default()
10349                });
10350            }
10351            self.advance();
10352            parts.push(next_tok.value);
10353        }
10354        Ok(parts.join("."))
10355    }
10356
10357    /// Parse: `publish ChannelName within ShieldName` — Publish-Ext (D8).
10358    fn parse_publish_step(&mut self) -> Result<FlowStep, ParseError> {
10359        let tok = self.consume(TokenType::Publish)?;
10360        let channel = self.consume(TokenType::Identifier)?.value;
10361        self.consume(TokenType::Within)?;
10362        let shield = self.consume(TokenType::Identifier)?.value;
10363        Ok(FlowStep::Publish(PublishStatement {
10364            channel_ref: channel,
10365            shield_ref: shield,
10366            loc: Loc {
10367                line: tok.line,
10368                column: tok.column,
10369            },
10370        }))
10371    }
10372
10373    /// Parse: `discover ChannelName as alias` — dual of publish.
10374    fn parse_discover_step(&mut self) -> Result<FlowStep, ParseError> {
10375        let tok = self.consume(TokenType::Discover)?;
10376        let cap = self.consume(TokenType::Identifier)?.value;
10377        self.consume(TokenType::As)?;
10378        let alias = self.consume(TokenType::Identifier)?.value;
10379        Ok(FlowStep::Discover(DiscoverStatement {
10380            capability_ref: cap,
10381            alias,
10382            loc: Loc {
10383                line: tok.line,
10384                column: tok.column,
10385            },
10386        }))
10387    }
10388}
10389
10390// ── §λ-L-E Fase 13 — Mobile Typed Channels parser tests ─────────────────────
10391
10392#[cfg(test)]
10393mod fase13_parser_tests {
10394    use super::*;
10395    use crate::lexer::Lexer;
10396
10397    fn parse(src: &str) -> Result<Program, ParseError> {
10398        let tokens = Lexer::new(src, "<test>").tokenize().expect("lex");
10399        Parser::new(tokens).parse()
10400    }
10401
10402    #[test]
10403    fn channel_full_parses() {
10404        let src = r#"channel C { message: Order qos: at_least_once lifetime: affine persistence: ephemeral shield: Gate }"#;
10405        let prog = parse(src).expect("parse");
10406        match &prog.declarations[0] {
10407            Declaration::Channel(c) => {
10408                assert_eq!(c.name, "C");
10409                assert_eq!(c.message, "Order");
10410                assert_eq!(c.qos, "at_least_once");
10411                assert_eq!(c.lifetime, "affine");
10412                assert_eq!(c.persistence, "ephemeral");
10413                assert_eq!(c.shield_ref, "Gate");
10414            }
10415            _ => panic!("expected ChannelDefinition"),
10416        }
10417    }
10418
10419    #[test]
10420    fn channel_defaults_match_paper_d1() {
10421        let prog = parse("channel C { message: Order }").expect("parse");
10422        if let Declaration::Channel(c) = &prog.declarations[0] {
10423            assert_eq!(c.qos, "at_least_once"); // default
10424            assert_eq!(c.lifetime, "affine"); // D1 default
10425            assert_eq!(c.persistence, "ephemeral");
10426            assert_eq!(c.shield_ref, "");
10427        } else {
10428            panic!("expected ChannelDefinition");
10429        }
10430    }
10431
10432    #[test]
10433    fn channel_second_order_message_type_parses() {
10434        let prog = parse("channel C { message: Channel<Order> }").expect("parse");
10435        if let Declaration::Channel(c) = &prog.declarations[0] {
10436            assert_eq!(c.message, "Channel<Order>");
10437        } else {
10438            panic!("expected ChannelDefinition");
10439        }
10440    }
10441
10442    #[test]
10443    fn channel_nested_channel_message_type_parses() {
10444        let prog = parse("channel C { message: Channel<Channel<Order>> }").expect("parse");
10445        if let Declaration::Channel(c) = &prog.declarations[0] {
10446            assert_eq!(c.message, "Channel<Channel<Order>>");
10447        } else {
10448            panic!("expected ChannelDefinition");
10449        }
10450    }
10451
10452    #[test]
10453    fn channel_invalid_qos_rejected() {
10454        let err = parse("channel C { message: T qos: bogus }").unwrap_err();
10455        assert!(err.message.contains("Invalid qos"), "got {}", err.message);
10456    }
10457
10458    #[test]
10459    fn channel_invalid_lifetime_rejected() {
10460        let err = parse("channel C { message: T lifetime: eternal }").unwrap_err();
10461        assert!(
10462            err.message.contains("Invalid lifetime"),
10463            "got {}",
10464            err.message
10465        );
10466    }
10467
10468    #[test]
10469    fn channel_invalid_persistence_rejected() {
10470        let err = parse("channel C { message: T persistence: forever }").unwrap_err();
10471        assert!(
10472            err.message.contains("Invalid persistence"),
10473            "got {}",
10474            err.message
10475        );
10476    }
10477
10478    #[test]
10479    fn emit_value_parses() {
10480        let src = "flow f() -> Out { emit C(payload) }";
10481        let prog = parse(src).expect("parse");
10482        if let Declaration::Flow(f) = &prog.declarations[0] {
10483            match &f.body[0] {
10484                FlowStep::Emit(e) => {
10485                    assert_eq!(e.channel_ref, "C");
10486                    assert_eq!(e.value_ref, "payload");
10487                }
10488                other => panic!("expected Emit, got {:?}", other),
10489            }
10490        } else {
10491            panic!("expected Flow");
10492        }
10493    }
10494
10495    #[test]
10496    fn publish_within_shield_parses() {
10497        let src = "flow f() -> Cap { publish C within Gate }";
10498        let prog = parse(src).expect("parse");
10499        if let Declaration::Flow(f) = &prog.declarations[0] {
10500            match &f.body[0] {
10501                FlowStep::Publish(p) => {
10502                    assert_eq!(p.channel_ref, "C");
10503                    assert_eq!(p.shield_ref, "Gate");
10504                }
10505                other => panic!("expected Publish, got {:?}", other),
10506            }
10507        } else {
10508            panic!("expected Flow");
10509        }
10510    }
10511
10512    #[test]
10513    fn discover_with_alias_parses() {
10514        let src = "flow f() -> Out { discover C as ch }";
10515        let prog = parse(src).expect("parse");
10516        if let Declaration::Flow(f) = &prog.declarations[0] {
10517            match &f.body[0] {
10518                FlowStep::Discover(d) => {
10519                    assert_eq!(d.capability_ref, "C");
10520                    assert_eq!(d.alias, "ch");
10521                }
10522                other => panic!("expected Discover, got {:?}", other),
10523            }
10524        } else {
10525            panic!("expected Flow");
10526        }
10527    }
10528
10529    #[test]
10530    fn listen_typed_ref_sets_flag_true() {
10531        let src = "daemon D() { goal: \"x\" listen C as ev { } }";
10532        let prog = parse(src).expect("parse");
10533        if let Declaration::Daemon(d) = &prog.declarations[0] {
10534            assert_eq!(d.listeners.len(), 1);
10535            assert_eq!(d.listeners[0].channel, "C");
10536            assert!(d.listeners[0].channel_is_ref, "typed ref ⇒ true");
10537        } else {
10538            panic!("expected Daemon");
10539        }
10540    }
10541
10542    #[test]
10543    fn listen_string_topic_legacy_flag_false() {
10544        let src = "daemon D() { goal: \"x\" listen \"orders\" as ev { } }";
10545        let prog = parse(src).expect("parse");
10546        if let Declaration::Daemon(d) = &prog.declarations[0] {
10547            assert_eq!(d.listeners.len(), 1);
10548            assert_eq!(d.listeners[0].channel, "orders");
10549            assert!(!d.listeners[0].channel_is_ref, "string topic ⇒ false");
10550        } else {
10551            panic!("expected Daemon");
10552        }
10553    }
10554
10555    // ── Fase 13.i — emit value_ref accepts dotted access ───────────
10556
10557    fn extract_first_emit(prog: &Program) -> &EmitStatement {
10558        if let Declaration::Flow(f) = &prog.declarations[0] {
10559            if let FlowStep::Emit(e) = &f.body[0] {
10560                return e;
10561            }
10562        }
10563        panic!("expected emit statement at flow body[0]");
10564    }
10565
10566    #[test]
10567    fn emit_accepts_bare_identifier_value_ref() {
10568        // Pre-13.i baseline — must keep working.
10569        let prog = parse("flow f() -> Out { emit Hello(payload) }").expect("parse");
10570        let emit = extract_first_emit(&prog);
10571        assert_eq!(emit.channel_ref, "Hello");
10572        assert_eq!(emit.value_ref, "payload");
10573    }
10574
10575    #[test]
10576    fn emit_accepts_two_segment_dotted_value_ref() {
10577        // The exact case adopters reported as broken before 13.i.
10578        let prog = parse("flow f() -> Out { emit Hello(Build.output) }").expect("parse");
10579        let emit = extract_first_emit(&prog);
10580        assert_eq!(emit.value_ref, "Build.output");
10581    }
10582
10583    #[test]
10584    fn emit_accepts_three_segment_nested_dotted_value_ref() {
10585        let prog = parse("flow f() -> Out { emit Score(Analyze.result.score) }").expect("parse");
10586        let emit = extract_first_emit(&prog);
10587        assert_eq!(emit.value_ref, "Analyze.result.score");
10588    }
10589
10590    #[test]
10591    fn emit_dotted_with_trailing_dot_fails() {
10592        // Trailing `.` must still error — every '.' demands an identifier.
10593        let result = parse("flow f() -> Out { emit Hello(Build.) }");
10594        assert!(result.is_err(), "expected parse error for trailing dot");
10595    }
10596}
10597
10598// ── §Fase 14.a — declaration_trivia parallel channel tests ──────────────────
10599
10600#[cfg(test)]
10601mod fase14a_declaration_trivia_tests {
10602    use super::*;
10603    use crate::lexer::Lexer;
10604    use crate::tokens::TriviaKind;
10605
10606    fn parse(src: &str) -> Program {
10607        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
10608        Parser::new(toks).parse().expect("parse")
10609    }
10610
10611    #[test]
10612    fn no_comments_means_empty_trivia_per_decl() {
10613        let prog = parse("flow F() -> Out { }");
10614        assert_eq!(prog.declarations.len(), 1);
10615        assert_eq!(prog.declaration_trivia.len(), 1);
10616        assert!(prog.declaration_trivia[0].leading.is_empty());
10617        assert!(prog.declaration_trivia[0].trailing.is_empty());
10618    }
10619
10620    #[test]
10621    fn doc_line_comment_attaches_as_leading() {
10622        let prog = parse("/// Documents F\nflow F() -> Out { }");
10623        let triv = &prog.declaration_trivia[0];
10624        assert_eq!(triv.leading.len(), 1);
10625        assert_eq!(triv.leading[0].kind, TriviaKind::DocLine);
10626        assert!(triv.leading[0].is_doc());
10627        assert_eq!(triv.leading[0].text, "/// Documents F");
10628    }
10629
10630    #[test]
10631    fn regular_line_comment_attaches_as_leading() {
10632        let prog = parse("// header\nflow F() -> Out { }");
10633        let triv = &prog.declaration_trivia[0];
10634        assert_eq!(triv.leading.len(), 1);
10635        assert_eq!(triv.leading[0].kind, TriviaKind::Line);
10636        assert!(!triv.leading[0].is_doc());
10637    }
10638
10639    #[test]
10640    fn block_doc_comment_attaches_as_leading() {
10641        let prog = parse("/** Doc block */\nflow F() -> Out { }");
10642        let triv = &prog.declaration_trivia[0];
10643        assert_eq!(triv.leading[0].kind, TriviaKind::DocBlock);
10644        assert!(triv.leading[0].is_doc());
10645    }
10646
10647    #[test]
10648    fn multiple_comments_collected_in_source_order() {
10649        let src = "/// First\n/// Second\nflow F() -> Out { }";
10650        let prog = parse(src);
10651        let triv = &prog.declaration_trivia[0];
10652        assert_eq!(triv.leading.len(), 2);
10653        assert_eq!(triv.leading[0].text, "/// First");
10654        assert_eq!(triv.leading[1].text, "/// Second");
10655    }
10656
10657    #[test]
10658    fn three_decls_each_get_own_leading() {
10659        let src = "/// for A\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }\n/// for C\nflow C() -> Out { }";
10660        let prog = parse(src);
10661        assert_eq!(prog.declarations.len(), 3);
10662        assert_eq!(prog.declaration_trivia.len(), 3);
10663        for (idx, name) in ["A", "B", "C"].iter().enumerate() {
10664            let triv = &prog.declaration_trivia[idx];
10665            assert_eq!(triv.leading.len(), 1);
10666            assert_eq!(triv.leading[0].text, format!("/// for {name}"));
10667        }
10668    }
10669
10670    #[test]
10671    fn trailing_comment_attaches_to_last_token_of_decl() {
10672        // Comment on the same line as the decl's closing brace.
10673        let prog = parse("flow F() -> Out { } // tail");
10674        let triv = &prog.declaration_trivia[0];
10675        assert_eq!(triv.trailing.len(), 1);
10676        assert_eq!(triv.trailing[0].text, "// tail");
10677    }
10678
10679    #[test]
10680    fn mixed_doc_and_regular_preserve_order_between_decls() {
10681        let src = "/// doc for A\nflow A() -> Out { }\n\n// header line\n/// doc for B\nflow B() -> Out { }";
10682        let prog = parse(src);
10683        assert_eq!(prog.declarations.len(), 2);
10684        // A: just the doc comment.
10685        assert_eq!(prog.declaration_trivia[0].leading.len(), 1);
10686        // B: header + doc, in source order.
10687        assert_eq!(prog.declaration_trivia[1].leading.len(), 2);
10688        assert_eq!(prog.declaration_trivia[1].leading[0].text, "// header line");
10689        assert_eq!(prog.declaration_trivia[1].leading[1].text, "/// doc for B");
10690    }
10691
10692    #[test]
10693    fn parser_unaffected_by_comments_in_grammar_path() {
10694        // The parser must accept comments interleaved between every
10695        // legal token without affecting the AST shape it produces.
10696        // This is the regression guard for "lossless lexing must not
10697        // change parsing semantics."
10698        let src =
10699            "// before flow\nflow /* between flow and name */ F() -> Out {\n  // body comment\n}";
10700        let prog = parse(src);
10701        assert_eq!(prog.declarations.len(), 1);
10702        if let Declaration::Flow(f) = &prog.declarations[0] {
10703            assert_eq!(f.name, "F");
10704        } else {
10705            panic!("expected Flow declaration");
10706        }
10707    }
10708}
10709
10710// ── §Fase 14.b — per-struct trivia fields tests ─────────────────────────────
10711//
10712// 14.b spreads `leading_trivia` / `trailing_trivia` into every Declaration
10713// variant struct (FlowDefinition, ChannelDefinition, PersonaDefinition, …).
10714// The Python AST already had this shape since 14.a; 14.b achieves Rust
10715// parity. The side-channel `Program.declaration_trivia` is preserved for
10716// backward compat — these tests verify the new direct access path.
10717
10718#[cfg(test)]
10719mod fase14b_per_struct_trivia_tests {
10720    use super::*;
10721    use crate::lexer::Lexer;
10722    use crate::tokens::TriviaKind;
10723
10724    fn parse(src: &str) -> Program {
10725        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
10726        Parser::new(toks).parse().expect("parse")
10727    }
10728
10729    #[test]
10730    fn flow_definition_carries_leading_trivia_directly() {
10731        let prog = parse("/// documents F\nflow F() -> Out { }");
10732        if let Declaration::Flow(f) = &prog.declarations[0] {
10733            assert_eq!(f.leading_trivia.len(), 1);
10734            assert_eq!(f.leading_trivia[0].kind, TriviaKind::DocLine);
10735            assert_eq!(f.leading_trivia[0].text, "/// documents F");
10736            assert!(f.trailing_trivia.is_empty());
10737        } else {
10738            panic!("expected Flow declaration");
10739        }
10740    }
10741
10742    #[test]
10743    fn flow_definition_carries_trailing_trivia_directly() {
10744        let prog = parse("flow F() -> Out { } // tail comment");
10745        if let Declaration::Flow(f) = &prog.declarations[0] {
10746            assert_eq!(f.trailing_trivia.len(), 1);
10747            assert_eq!(f.trailing_trivia[0].text, "// tail comment");
10748        } else {
10749            panic!("expected Flow declaration");
10750        }
10751    }
10752
10753    #[test]
10754    fn channel_definition_carries_trivia_directly() {
10755        // ChannelDefinition is a Tier-1 declaration; verify per-struct fields
10756        // populate just like FlowDefinition.
10757        let src = concat!(
10758            "/// inbound order events\n",
10759            "channel Orders {\n",
10760            "    message:     Order\n",
10761            "    qos:         at_least_once\n",
10762            "    lifetime:    affine\n",
10763            "    persistence: ephemeral\n",
10764            "    shield:      Broker\n",
10765            "}",
10766        );
10767        let prog = parse(src);
10768        if let Declaration::Channel(ch) = &prog.declarations[0] {
10769            assert_eq!(ch.leading_trivia.len(), 1);
10770            assert!(ch.leading_trivia[0].is_doc());
10771            assert_eq!(ch.leading_trivia[0].text, "/// inbound order events");
10772        } else {
10773            panic!("expected Channel declaration");
10774        }
10775    }
10776
10777    #[test]
10778    fn per_struct_fields_match_side_channel() {
10779        // 14.a side-channel and 14.b per-struct fields must hold identical
10780        // data — they are populated by the same parser pass.
10781        let src = "/// for A\n// header for B\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }";
10782        let prog = parse(src);
10783        for (idx, decl) in prog.declarations.iter().enumerate() {
10784            let side = &prog.declaration_trivia[idx];
10785            let (per_lead, per_trail) = match decl {
10786                Declaration::Flow(f) => (&f.leading_trivia, &f.trailing_trivia),
10787                _ => panic!("unexpected variant"),
10788            };
10789            assert_eq!(per_lead.len(), side.leading.len());
10790            assert_eq!(per_trail.len(), side.trailing.len());
10791            for (a, b) in per_lead.iter().zip(side.leading.iter()) {
10792                assert_eq!(a.text, b.text);
10793                assert_eq!(a.kind, b.kind);
10794            }
10795        }
10796    }
10797
10798    #[test]
10799    fn comment_free_program_yields_empty_per_struct_fields() {
10800        let prog = parse("flow F() -> Out { }");
10801        if let Declaration::Flow(f) = &prog.declarations[0] {
10802            assert!(f.leading_trivia.is_empty());
10803            assert!(f.trailing_trivia.is_empty());
10804        } else {
10805            panic!("expected Flow declaration");
10806        }
10807    }
10808}
10809
10810// ── §Fase 14.c — inner doc comments (//!, /*!) ──────────────────────────────
10811//
10812// Inner doc comments document the *enclosing* item rather than the next
10813// sibling. Today they flow through the trivia channel like any other
10814// comment; downstream consumers (axon doc, LSP) decide how to interpret
10815// `is_inner_doc()`. These tests verify the lexer→parser pipeline preserves
10816// the inner-doc discriminator end-to-end.
10817
10818#[cfg(test)]
10819mod fase14c_inner_doc_tests {
10820    use super::*;
10821    use crate::lexer::Lexer;
10822    use crate::tokens::TriviaKind;
10823
10824    fn parse(src: &str) -> Program {
10825        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
10826        Parser::new(toks).parse().expect("parse")
10827    }
10828
10829    #[test]
10830    fn inner_doc_line_reaches_leading_trivia() {
10831        let src = "//! file-level docs\nflow F() -> Out { }";
10832        let prog = parse(src);
10833        let triv = &prog.declaration_trivia[0];
10834        assert_eq!(triv.leading.len(), 1);
10835        assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocLine);
10836        assert!(triv.leading[0].is_doc());
10837        assert!(triv.leading[0].is_inner_doc());
10838        assert_eq!(triv.leading[0].text, "//! file-level docs");
10839        assert_eq!(triv.leading[0].stripped_text(), " file-level docs");
10840    }
10841
10842    #[test]
10843    fn inner_doc_block_reaches_leading_trivia() {
10844        let src = "/*! module-level docs */\nflow F() -> Out { }";
10845        let prog = parse(src);
10846        let triv = &prog.declaration_trivia[0];
10847        assert_eq!(triv.leading.len(), 1);
10848        assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocBlock);
10849        assert!(triv.leading[0].is_inner_doc());
10850        assert_eq!(triv.leading[0].stripped_text(), " module-level docs ");
10851    }
10852
10853    #[test]
10854    fn outer_and_inner_doc_can_coexist() {
10855        // File-level inner doc on top, then an outer doc for the
10856        // declaration. Both reach the trivia channel and remain
10857        // distinguishable via `is_inner_doc()`.
10858        let src = "//! file docs\n/// docs F\nflow F() -> Out { }";
10859        let prog = parse(src);
10860        let triv = &prog.declaration_trivia[0];
10861        assert_eq!(triv.leading.len(), 2);
10862        assert!(triv.leading[0].is_inner_doc());
10863        assert!(triv.leading[1].is_doc());
10864        assert!(!triv.leading[1].is_inner_doc());
10865    }
10866
10867    #[test]
10868    fn inner_doc_reaches_per_struct_fields() {
10869        // Same data must be visible via the per-struct fields (Fase 14.b).
10870        let src = "//! intro\nflow F() -> Out { }";
10871        let prog = parse(src);
10872        if let Declaration::Flow(f) = &prog.declarations[0] {
10873            assert_eq!(f.leading_trivia.len(), 1);
10874            assert!(f.leading_trivia[0].is_inner_doc());
10875        } else {
10876            panic!("expected Flow declaration");
10877        }
10878    }
10879}
10880
10881// ── §Fase 28.c — Parser error recovery test pack ─────────────────────────────
10882//
10883// Mirror of `tests/test_fase28_parser_recovery.py` (Python side, 28.b).
10884// The test classes here line up 1-1 with the Python ones so the cross-
10885// stack drift gate (28.i) can compare error-list shapes input-for-input.
10886//
10887// Test classes:
10888//   - backwards_compat: existing `parse()` API unchanged
10889//   - single_error_recovery: one bad decl → one error, rest parse OK
10890//   - multi_error_recovery: N independent errors → N entries
10891//   - sync_points: every top-level keyword resyncs correctly
10892//   - parse_result_api: `has_errors`, `is_clean`
10893//   - edge_cases: EOF mid-error, brace imbalance, only-bad-tokens
10894//   - robustness_fuzz: 1000 deterministic-seeded mutations never crash
10895//   - no_ghost_errors: single broken field produces exactly 1 error
10896//   - integration_with_colon_diagnostic: v1.19.4 hint preserved under
10897//     recovery mode
10898#[cfg(test)]
10899mod fase28_recovery_tests {
10900    use super::*;
10901    use crate::lexer::Lexer;
10902
10903    /// Lex a source and return tokens for the parser to consume.
10904    /// Mirrors the Python `_parse_recovery` helper.
10905    fn lex(src: &str) -> Vec<Token> {
10906        Lexer::new(src, "<test>").tokenize().expect("lex")
10907    }
10908
10909    /// Parse with recovery mode. Returns `(program, errors)` so call
10910    /// sites read like the Python helper.
10911    fn recover(src: &str) -> ParseResult {
10912        Parser::new(lex(src)).parse_with_recovery()
10913    }
10914
10915    /// Strict parse. Mirrors the Python `_parse_strict` helper.
10916    fn strict(src: &str) -> Result<Program, ParseError> {
10917        Parser::new(lex(src)).parse()
10918    }
10919
10920    // ── backwards_compat ─────────────────────────────────────────
10921
10922    #[test]
10923    fn strict_parse_unchanged_for_clean_source() {
10924        // The existing `parse()` API must continue to succeed
10925        // verbatim on every well-formed input — D9.
10926        let src = "intent I {}";
10927        let prog = strict(src).expect("clean parse");
10928        assert_eq!(prog.declarations.len(), 1);
10929    }
10930
10931    #[test]
10932    fn strict_parse_still_raises_on_first_error() {
10933        // D9 + D8: opt-in to recovery via `parse_with_recovery`;
10934        // strict mode must still bubble the first error.
10935        // (Using a parse-time error rather than a lex error — `@@@`
10936        // would be rejected by the lexer, which is out of scope.)
10937        let src = "flow F() { } not_a_keyword flow G() { }";
10938        let _ = strict(src).expect_err("must error fast in strict mode");
10939    }
10940
10941    #[test]
10942    fn recovery_clean_source_yields_no_errors() {
10943        let src = "flow F() { } flow G() { }";
10944        let pr = recover(src);
10945        assert!(pr.is_clean(), "errors: {:?}", pr.errors);
10946        assert_eq!(pr.program.declarations.len(), 2);
10947    }
10948
10949    // ── single_error_recovery ────────────────────────────────────
10950
10951    #[test]
10952    fn single_unknown_top_level_token_recovers() {
10953        // One garbage token at top level; rest must parse.
10954        let src = "garbage_token flow F() { } flow G() { }";
10955        let pr = recover(src);
10956        assert_eq!(pr.errors.len(), 1, "errors: {:?}", pr.errors);
10957        assert_eq!(pr.program.declarations.len(), 2);
10958    }
10959
10960    #[test]
10961    fn error_in_first_decl_does_not_block_second() {
10962        // `flow F` body refers to non-keyword `nope`; the error
10963        // recovery must skip to the next top-level keyword.
10964        let src = "flow F() { not_a_step nope } flow G() { }";
10965        let pr = recover(src);
10966        assert!(pr.has_errors(), "expected at least one error");
10967        // The second flow must be reachable.
10968        let names: Vec<&str> = pr
10969            .program
10970            .declarations
10971            .iter()
10972            .filter_map(|d| match d {
10973                Declaration::Flow(f) => Some(f.name.as_str()),
10974                _ => None,
10975            })
10976            .collect();
10977        assert!(names.contains(&"G"), "G not found among {names:?}");
10978    }
10979
10980    #[test]
10981    fn malformed_declaration_then_clean_intent_recovers() {
10982        let src = "flow @ () { } intent I {}";
10983        let pr = recover(src);
10984        assert!(pr.has_errors());
10985        let kinds: Vec<&str> = pr
10986            .program
10987            .declarations
10988            .iter()
10989            .map(|d| match d {
10990                Declaration::Intent(_) => "intent",
10991                Declaration::Flow(_) => "flow",
10992                _ => "other",
10993            })
10994            .collect();
10995        assert!(kinds.contains(&"intent"), "kinds: {kinds:?}");
10996    }
10997
10998    #[test]
10999    fn recovery_does_not_double_count_a_single_error() {
11000        // Regression for the "ghost error" pathology that surfaced
11001        // during 28.b dev: a nested-decl error must not also fire
11002        // an "Unexpected token at top level" from the outer loop.
11003        // The Rust grammar has stricter intra-flow requirements
11004        // than Python; the invariant we assert here is that the
11005        // outer loop emits zero "Unexpected token at top level"
11006        // errors after an inner step-shape error.
11007        let src = "flow F() { not_a_step }";
11008        let pr = recover(src);
11009        let outer_ghosts = pr
11010            .errors
11011            .iter()
11012            .filter(|e| e.message.contains("at top level"))
11013            .count();
11014        assert_eq!(outer_ghosts, 0, "ghost errors: {:?}", pr.errors);
11015    }
11016
11017    // ── multi_error_recovery ─────────────────────────────────────
11018
11019    #[test]
11020    fn three_independent_errors_yield_three_entries() {
11021        let src =
11022            "garbage1 flow F() { } garbage2 flow G() { } garbage3 flow H() { }";
11023        let pr = recover(src);
11024        assert_eq!(pr.errors.len(), 3, "errors: {:?}", pr.errors);
11025        assert_eq!(pr.program.declarations.len(), 3);
11026    }
11027
11028    #[test]
11029    fn all_errors_no_valid_declarations() {
11030        let src = "foo bar baz qux";
11031        let pr = recover(src);
11032        assert!(pr.has_errors());
11033        assert!(pr.program.declarations.is_empty());
11034    }
11035
11036    #[test]
11037    fn errors_recorded_in_source_order() {
11038        let src = "x flow A() { } y flow B() { } z flow C() { }";
11039        let pr = recover(src);
11040        assert_eq!(pr.errors.len(), 3);
11041        let lines: Vec<u32> = pr.errors.iter().map(|e| e.line).collect();
11042        // Same source-line means we compare by column ordering;
11043        // either way they must be non-decreasing.
11044        assert!(
11045            lines.windows(2).all(|w| w[0] <= w[1]),
11046            "errors out of order: {lines:?}"
11047        );
11048    }
11049
11050    // ── sync_points ──────────────────────────────────────────────
11051
11052    #[test]
11053    fn sync_to_flow_keyword() {
11054        let src = "garbage flow F() { }";
11055        let pr = recover(src);
11056        assert_eq!(pr.program.declarations.len(), 1);
11057    }
11058
11059    #[test]
11060    fn sync_to_intent_keyword() {
11061        let src = "garbage intent I {}";
11062        let pr = recover(src);
11063        assert_eq!(pr.program.declarations.len(), 1);
11064    }
11065
11066    #[test]
11067    fn sync_to_persona_keyword() {
11068        let src = "garbage persona P { name: \"P\" role: \"R\" }";
11069        let pr = recover(src);
11070        assert!(
11071            pr.program
11072                .declarations
11073                .iter()
11074                .any(|d| matches!(d, Declaration::Persona(_))),
11075            "persona not recovered: decls = {:?}",
11076            pr.program.declarations.len()
11077        );
11078    }
11079
11080    #[test]
11081    fn sync_to_run_keyword() {
11082        let src = "garbage run R { input: { user_message: \"hi\" } }";
11083        let pr = recover(src);
11084        // Either Run was parsed, or recovery still produced ≥1 err.
11085        assert!(pr.has_errors());
11086    }
11087
11088    // ── parse_result_api ─────────────────────────────────────────
11089
11090    #[test]
11091    fn parse_result_has_errors_and_is_clean_invert() {
11092        let pr_clean = recover("flow F() { }");
11093        assert!(pr_clean.is_clean());
11094        assert!(!pr_clean.has_errors());
11095
11096        let pr_err = recover("garbage");
11097        assert!(!pr_err.is_clean());
11098        assert!(pr_err.has_errors());
11099    }
11100
11101    #[test]
11102    fn parse_result_program_field_holds_partial_program() {
11103        let pr = recover("garbage flow F() { }");
11104        assert!(!pr.program.declarations.is_empty());
11105    }
11106
11107    #[test]
11108    fn parse_result_errors_carry_line_and_column() {
11109        let pr = recover("garbage");
11110        assert!(!pr.errors.is_empty());
11111        let e = &pr.errors[0];
11112        assert!(e.line >= 1);
11113        // Column may be 0-based or 1-based depending on lexer;
11114        // accept anything ≥ 0.
11115        let _ = e.column;
11116        assert!(!e.message.is_empty());
11117    }
11118
11119    #[test]
11120    fn parse_result_debug_renders() {
11121        let pr = recover("flow F() { }");
11122        let s = format!("{pr:?}");
11123        assert!(s.contains("ParseResult"));
11124    }
11125
11126    // ── edge_cases ───────────────────────────────────────────────
11127
11128    #[test]
11129    fn empty_source_is_clean() {
11130        let pr = recover("");
11131        assert!(pr.is_clean());
11132        assert!(pr.program.declarations.is_empty());
11133    }
11134
11135    #[test]
11136    fn whitespace_only_source_is_clean() {
11137        let pr = recover("   \n\n\t  \n");
11138        assert!(pr.is_clean());
11139        assert!(pr.program.declarations.is_empty());
11140    }
11141
11142    #[test]
11143    fn only_garbage_does_not_crash() {
11144        // Lex-clean garbage tokens (avoids AxonLexerError).
11145        let pr = recover("foo bar baz { qux quux } corge { grault }");
11146        assert!(pr.has_errors());
11147    }
11148
11149    #[test]
11150    fn unbalanced_close_brace_does_not_crash() {
11151        let pr = recover("} flow F() { }");
11152        // Recovery must keep walking past stray `}`.
11153        let names: Vec<&str> = pr
11154            .program
11155            .declarations
11156            .iter()
11157            .filter_map(|d| match d {
11158                Declaration::Flow(f) => Some(f.name.as_str()),
11159                _ => None,
11160            })
11161            .collect();
11162        assert!(names.contains(&"F"), "F not recovered: {names:?}");
11163    }
11164
11165    #[test]
11166    fn error_at_eof_does_not_loop() {
11167        // Truncated declaration. Must terminate; finite errors.
11168        let pr = recover("flow F() { ");
11169        // Either errored or somehow accepted — but must terminate.
11170        let _ = pr.errors.len();
11171    }
11172
11173    #[test]
11174    fn nested_braces_inside_error_still_balance() {
11175        // Walker must respect brace depth so a `}` inside a malformed
11176        // block does not prematurely sync.
11177        let src = "flow F() { not_a_step { inner } } flow G() { }";
11178        let pr = recover(src);
11179        let names: Vec<&str> = pr
11180            .program
11181            .declarations
11182            .iter()
11183            .filter_map(|d| match d {
11184                Declaration::Flow(f) => Some(f.name.as_str()),
11185                _ => None,
11186            })
11187            .collect();
11188        assert!(names.contains(&"G"), "G not recovered: {names:?}");
11189    }
11190
11191    // ── robustness_fuzz ──────────────────────────────────────────
11192    //
11193    // Deterministic-seeded mutator (xorshift). 100 buckets ×
11194    // 10 mutations = 1000 iterations, byte-bounded so fuzz time
11195    // stays under 1 s on a release build. Recovery must NEVER crash;
11196    // lexer-level errors are out of scope (lexer recovery is its own
11197    // sub-fase). 28.b mirrors this with the same structure.
11198
11199    #[derive(Clone, Copy)]
11200    struct Xorshift(u64);
11201    impl Xorshift {
11202        fn next(&mut self) -> u64 {
11203            let mut x = self.0;
11204            x ^= x << 13;
11205            x ^= x >> 7;
11206            x ^= x << 17;
11207            self.0 = x;
11208            x
11209        }
11210        fn pick<T: Copy>(&mut self, slice: &[T]) -> T {
11211            slice[(self.next() as usize) % slice.len()]
11212        }
11213    }
11214
11215    fn mutate(src: &str, rng: &mut Xorshift) -> String {
11216        let mut bytes: Vec<u8> = src.bytes().collect();
11217        if bytes.is_empty() {
11218            return src.to_string();
11219        }
11220        let op = rng.next() % 4;
11221        let pos = (rng.next() as usize) % bytes.len();
11222        // Stick to ASCII-safe printable bytes to keep input lex-able
11223        // most of the time. AxonLexerError is still possible and is
11224        // tolerated by the recovery contract.
11225        let safe: &[u8] = b"abcdefghijklmnopqrstuvwxyz {}();:,_0123456789";
11226        match op {
11227            0 => {
11228                bytes.remove(pos);
11229            }
11230            1 => {
11231                let b = rng.pick(safe);
11232                bytes.insert(pos, b);
11233            }
11234            2 if pos + 1 < bytes.len() => {
11235                bytes.swap(pos, pos + 1);
11236            }
11237            _ => {
11238                let b = rng.pick(safe);
11239                bytes[pos] = b;
11240            }
11241        }
11242        // Lossy decode: mutator may have produced invalid UTF-8;
11243        // strip non-ASCII before handing to the lexer.
11244        bytes.retain(|b| b.is_ascii());
11245        String::from_utf8_lossy(&bytes).into_owned()
11246    }
11247
11248    #[test]
11249    fn fuzz_recovery_never_crashes() {
11250        let seed_bases = [
11251            "flow F() { }",
11252            "intent I { }",
11253            "persona P { name: \"P\" role: \"R\" }",
11254            "intent J { ask: \"a\" }",
11255            "type T = String",
11256        ];
11257        // 100 buckets × 10 mutations = 1000 iterations, deterministic.
11258        for (bucket, base) in (0..100u64).zip(seed_bases.iter().cycle()) {
11259            let mut rng = Xorshift(0x1234_5678_9abc_def0_u64.wrapping_add(bucket));
11260            let mut current = (*base).to_string();
11261            for _ in 0..10 {
11262                current = mutate(&current, &mut rng);
11263                // Lexer may reject; that's outside parser-recovery
11264                // scope (28.b/c). Skip those iterations.
11265                let toks = match Lexer::new(&current, "<fuzz>").tokenize() {
11266                    Ok(t) => t,
11267                    Err(_) => continue,
11268                };
11269                // Recovery must not panic on any well-lexed input.
11270                let _pr = Parser::new(toks).parse_with_recovery();
11271            }
11272        }
11273    }
11274
11275    // ── integration_with_v1_19_4_colon_diagnostic ────────────────
11276
11277    #[test]
11278    fn missing_colon_hint_preserved_under_recovery() {
11279        // The Rust frontend's strict `parse()` carries the same
11280        // colon diagnostic shape as the Python side. Recovery mode
11281        // must not erase it.
11282        let src = "flow F() { run R { input { user_message: \"hi\" } } }";
11283        let pr = recover(src);
11284        // Either the parser accepts this (some shape may be valid)
11285        // or it errors — but if it errors, the message must surface
11286        // the diagnostic content.
11287        if !pr.errors.is_empty() {
11288            let any_msg = pr.errors.iter().any(|e| !e.message.is_empty());
11289            assert!(any_msg);
11290        }
11291    }
11292
11293    // ── recovery preserves declaration ordering ──────────────────
11294
11295    #[test]
11296    fn recovered_declarations_appear_in_source_order() {
11297        let src = "flow A() { } garbage flow B() { } garbage flow C() { }";
11298        let pr = recover(src);
11299        let names: Vec<&str> = pr
11300            .program
11301            .declarations
11302            .iter()
11303            .filter_map(|d| match d {
11304                Declaration::Flow(f) => Some(f.name.as_str()),
11305                _ => None,
11306            })
11307            .collect();
11308        assert_eq!(names, vec!["A", "B", "C"]);
11309    }
11310}
11311
11312// ── §Fase 28.d — Source-context diagnostic block test pack ───────────────────
11313//
11314// Mirror of `tests/test_fase28_source_context.py` (Python side, 28.d).
11315// The render output must be byte-identical to the Python `SourceSnippet.render`
11316// on the same input — D7 ratified (cross-stack drift gate). Golden strings
11317// in `golden_*` tests are duplicated verbatim in the Python pack; edits
11318// here MUST be mirrored on the Python side and vice versa.
11319#[cfg(test)]
11320mod fase28_source_context_tests {
11321    use super::*;
11322    use crate::lexer::Lexer;
11323
11324    fn snippet(source: &str, line: u32, column: u32, filename: &str) -> String {
11325        SourceSnippet::new(
11326            source.to_string(),
11327            line,
11328            column,
11329            filename.to_string(),
11330        )
11331        .render()
11332    }
11333
11334    // ── Pure rendering ──────────────────────────────────────────
11335
11336    #[test]
11337    fn rustc_style_block_for_middle_line() {
11338        let src = "line one\nline two\nline three\nline four\nline five";
11339        let out = snippet(src, 3, 6, "x.axon");
11340        assert!(out.contains("--> x.axon:3:6"));
11341        assert!(out.contains("1 | line one"));
11342        assert!(out.contains("2 | line two"));
11343        assert!(out.contains("3 | line three"));
11344        assert!(out.contains("4 | line four"));
11345        assert!(out.contains("5 | line five"));
11346        // Caret col 6 → 5-space pad. Empty gutter is 1 space (gutter=1).
11347        assert!(out.contains("\n  |      ^"), "out:\n{out}");
11348    }
11349
11350    #[test]
11351    fn caret_column_one_renders_correctly() {
11352        let out = snippet("abc\n", 1, 1, "<source>");
11353        assert!(out.contains("\n  | ^"));
11354    }
11355
11356    #[test]
11357    fn first_line_clamps_context_before_to_zero() {
11358        let src = "first\nsecond\nthird\nfourth\nfifth";
11359        let out = snippet(src, 1, 1, "<source>");
11360        assert!(out.contains("1 | first"));
11361        assert!(out.contains("2 | second"));
11362        assert!(out.contains("3 | third"));
11363        assert!(!out.contains("4 | fourth"));
11364    }
11365
11366    #[test]
11367    fn last_line_clamps_context_after_to_eof() {
11368        let src = "first\nsecond\nthird\nfourth\nfifth";
11369        let out = snippet(src, 5, 2, "<source>");
11370        assert!(out.contains("5 | fifth"));
11371        assert!(out.contains("3 | third"));
11372        assert!(out.contains("4 | fourth"));
11373        assert!(!out.contains("2 | second"));
11374    }
11375
11376    #[test]
11377    fn gutter_width_grows_with_line_count() {
11378        let src: String = (1..=12).map(|i| format!("line{i}")).collect::<Vec<_>>().join("\n");
11379        let out = snippet(&src, 12, 1, "<source>");
11380        assert!(out.contains("12 | line12"));
11381        assert!(out.contains("10 | line10"));
11382    }
11383
11384    // ── Edge cases ──────────────────────────────────────────────
11385
11386    #[test]
11387    fn empty_source_returns_empty() {
11388        assert_eq!(snippet("", 1, 1, "<source>"), "");
11389    }
11390
11391    #[test]
11392    fn zero_line_returns_empty() {
11393        assert_eq!(snippet("hi", 0, 1, "<source>"), "");
11394    }
11395
11396    #[test]
11397    fn out_of_range_line_returns_empty() {
11398        assert_eq!(snippet("hi", 99, 1, "<source>"), "");
11399    }
11400
11401    #[test]
11402    fn caret_clamps_past_eol() {
11403        let out = snippet("hello", 1, 50, "<source>");
11404        assert!(out.contains("\n  |      ^"), "out:\n{out}");
11405    }
11406
11407    #[test]
11408    fn unicode_codepoint_count_for_caret_clamp() {
11409        // "héllo" = 5 codepoints; column past EOL clamps to 6.
11410        let out = snippet("héllo", 1, 99, "<source>");
11411        assert!(out.contains("\n  |      ^"), "out:\n{out}");
11412    }
11413
11414    #[test]
11415    fn trailing_newline_does_not_create_phantom_last_line() {
11416        let out = snippet("first\nsecond\n", 2, 1, "<source>");
11417        assert!(!out.contains("3 |"));
11418        assert!(out.contains("2 | second"));
11419    }
11420
11421    // ── Parser attach plumbing ──────────────────────────────────
11422
11423    fn lex(src: &str) -> Vec<Token> {
11424        Lexer::new(src, "<test>").tokenize().expect("lex")
11425    }
11426
11427    #[test]
11428    fn strict_parse_attaches_snippet_when_source_given() {
11429        let src = "garbage_token\nflow F() { }";
11430        let err = Parser::new(lex(src))
11431            .with_source(src, "x.axon")
11432            .parse()
11433            .expect_err("must error");
11434        assert!(err.source_snippet.is_some());
11435        let display = format!("{err}");
11436        assert!(display.contains("--> x.axon:"), "display: {display}");
11437    }
11438
11439    #[test]
11440    fn strict_parse_no_snippet_when_no_source() {
11441        let src = "garbage_token";
11442        let err = Parser::new(lex(src)).parse().expect_err("must error");
11443        assert!(err.source_snippet.is_none());
11444        let display = format!("{err}");
11445        assert!(!display.contains("\n  -->"));
11446    }
11447
11448    #[test]
11449    fn every_recovered_error_has_snippet() {
11450        let src = "garbage1\nflow F() { }\ngarbage2\nflow G() { }";
11451        let result = Parser::new(lex(src))
11452            .with_source(src, "multi.axon")
11453            .parse_with_recovery();
11454        assert!(!result.errors.is_empty());
11455        for err in &result.errors {
11456            assert!(err.source_snippet.is_some());
11457            let display = format!("{err}");
11458            assert!(
11459                display.contains("--> multi.axon:"),
11460                "display: {display}"
11461            );
11462        }
11463    }
11464
11465    #[test]
11466    fn recovery_no_snippet_when_no_source() {
11467        let src = "garbage1 garbage2";
11468        let result = Parser::new(lex(src)).parse_with_recovery();
11469        for err in &result.errors {
11470            assert!(err.source_snippet.is_none());
11471        }
11472    }
11473
11474    #[test]
11475    fn snippet_points_at_correct_line_for_each_error() {
11476        let src = "garbage_a\nflow F() { }\ngarbage_b\nflow G() { }";
11477        let result = Parser::new(lex(src))
11478            .with_source(src, "x")
11479            .parse_with_recovery();
11480        for err in &result.errors {
11481            let sn = err.source_snippet.as_ref().expect("snippet");
11482            assert_eq!(sn.line, err.line);
11483        }
11484    }
11485
11486    // ── Backwards-compat ────────────────────────────────────────
11487
11488    #[test]
11489    fn legacy_constructor_still_works() {
11490        let src = "flow F() { }";
11491        let prog = Parser::new(lex(src)).parse().expect("clean");
11492        assert_eq!(prog.declarations.len(), 1);
11493    }
11494
11495    #[test]
11496    fn attach_source_idempotent() {
11497        let err = ParseError {
11498            message: "bad".to_string(),
11499            line: 2,
11500            column: 3,
11501            ..Default::default()
11502        };
11503        let err2 = err.clone().attach_source("a\nb\nc\n", "f.axon");
11504        let first = format!("{err2}");
11505        let err3 = err.attach_source("a\nb\nc\n", "f.axon");
11506        let second = format!("{err3}");
11507        assert_eq!(first, second);
11508    }
11509
11510    #[test]
11511    fn attach_source_noop_when_line_zero() {
11512        let err = ParseError {
11513            message: "bad".to_string(),
11514            line: 0,
11515            column: 0,
11516            ..Default::default()
11517        };
11518        let err = err.attach_source("a\nb\nc\n", "f.axon");
11519        assert!(err.source_snippet.is_none());
11520    }
11521
11522    // ── Cross-stack golden parity ───────────────────────────────
11523    // These golden strings are duplicated verbatim in the Python
11524    // test pack at `tests/test_fase28_source_context.py::TestRustParityShape`.
11525    // Edits here MUST be mirrored in the Python pack — D7.
11526
11527    #[test]
11528    fn golden_simple_three_line_block() {
11529        let src = "alpha\nbeta\ngamma";
11530        let out = snippet(src, 2, 3, "g.axon");
11531        // Note: gutter=1, so empty_gutter=" " (one space). The
11532        // " --> ..." line therefore starts with two spaces ("<empty>"
11533        // + literal " --> ...").
11534        let expected = concat!(
11535            "  --> g.axon:2:3\n",
11536            "  |\n",
11537            "1 | alpha\n",
11538            "2 | beta\n",
11539            "  |   ^\n",
11540            "3 | gamma",
11541        );
11542        assert_eq!(out, expected);
11543    }
11544
11545    #[test]
11546    fn golden_first_line_caret() {
11547        let src = "abc\ndef\n";
11548        let out = snippet(src, 1, 1, "x");
11549        let expected = concat!(
11550            "  --> x:1:1\n",
11551            "  |\n",
11552            "1 | abc\n",
11553            "  | ^\n",
11554            "2 | def",
11555        );
11556        assert_eq!(out, expected);
11557    }
11558
11559    #[test]
11560    fn golden_two_digit_gutter() {
11561        let src: String = (1..=11)
11562            .map(|i| format!("L{i}"))
11563            .collect::<Vec<_>>()
11564            .join("\n");
11565        let out = snippet(&src, 10, 2, "big");
11566        let expected = concat!(
11567            "   --> big:10:2\n",
11568            "   |\n",
11569            " 8 | L8\n",
11570            " 9 | L9\n",
11571            "10 | L10\n",
11572            "   |  ^\n",
11573            "11 | L11",
11574        );
11575        assert_eq!(out, expected);
11576    }
11577}
11578
11579// ── §Fase 28.e — Parser integration tests for smart-suggest ──────────────────
11580//
11581// Mirror of `tests/test_fase28_smart_suggest.py::TestParserIntegration`.
11582// Verifies that the parser actually wires `suggest_for` into the
11583// unknown-keyword diagnostic at both error sites — top-level and
11584// flow-body.
11585#[cfg(test)]
11586mod fase28_smart_suggest_parser_tests {
11587    use super::*;
11588    use crate::lexer::Lexer;
11589
11590    fn lex(src: &str) -> Vec<Token> {
11591        Lexer::new(src, "<test>").tokenize().expect("lex")
11592    }
11593
11594    #[test]
11595    fn top_level_typo_suggests_flow() {
11596        let src = "flwo F() { }";
11597        let err = Parser::new(lex(src)).parse().expect_err("must error");
11598        assert!(
11599            err.message.contains("Did you mean `flow`?"),
11600            "msg: {}",
11601            err.message
11602        );
11603    }
11604
11605    #[test]
11606    fn top_level_unknown_far_no_suggestion() {
11607        let src = "qwerty F() { }";
11608        let err = Parser::new(lex(src)).parse().expect_err("must error");
11609        assert!(
11610            !err.message.contains("Did you mean"),
11611            "msg: {}",
11612            err.message
11613        );
11614    }
11615
11616    #[test]
11617    fn flow_body_typo_suggests_step() {
11618        let src = "flow F() { stepp S {} }";
11619        let err = Parser::new(lex(src)).parse().expect_err("must error");
11620        assert!(
11621            err.message.contains("Did you mean `step`"),
11622            "msg: {}",
11623            err.message
11624        );
11625    }
11626
11627    #[test]
11628    fn flow_body_typo_suggests_reason() {
11629        let src = "flow F() { reasn R {} }";
11630        let err = Parser::new(lex(src)).parse().expect_err("must error");
11631        assert!(
11632            err.message.contains("Did you mean `reason`?"),
11633            "msg: {}",
11634            err.message
11635        );
11636    }
11637
11638    #[test]
11639    fn recovery_mode_carries_hint() {
11640        let src = "flwo F() { }";
11641        let result = Parser::new(lex(src)).parse_with_recovery();
11642        assert!(
11643            result
11644                .errors
11645                .iter()
11646                .any(|e| e.message.contains("Did you mean `flow`?")),
11647            "errors: {:?}",
11648            result.errors
11649        );
11650    }
11651}
11652
11653// ── §Fase 35.m — mutate / purge where-clause capture ────────────────
11654
11655#[cfg(test)]
11656mod fase35m_mutate_purge_where_tests {
11657    use super::*;
11658
11659    fn parse(src: &str) -> Program {
11660        let tokens = crate::lexer::Lexer::new(src, "<test>")
11661            .tokenize()
11662            .expect("lex");
11663        Parser::new(tokens).parse().expect("parse")
11664    }
11665
11666    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
11667        for d in &prog.declarations {
11668            if let Declaration::Flow(f) = d {
11669                if f.name == flow {
11670                    return f.body.first().expect("flow has at least one step");
11671                }
11672            }
11673        }
11674        panic!("flow `{flow}` not found");
11675    }
11676
11677    #[test]
11678    fn mutate_captures_its_where_clause() {
11679        // Pre-35.m the `{ where: }` block was skipped — every mutate
11680        // ran whole-store. It must now reach `where_expr`.
11681        let prog =
11682            parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
11683        match first_step(&prog, "F") {
11684            FlowStep::Mutate(m) => {
11685                assert_eq!(m.store_name, "accounts");
11686                assert_eq!(m.where_expr, "id = 1");
11687            }
11688            other => panic!("expected Mutate, got {other:?}"),
11689        }
11690    }
11691
11692    #[test]
11693    fn purge_captures_its_where_clause() {
11694        let prog =
11695            parse("flow F() -> Unit { purge logs { where: \"ts < 100\" } }");
11696        match first_step(&prog, "F") {
11697            FlowStep::Purge(p) => {
11698                assert_eq!(p.store_name, "logs");
11699                assert_eq!(p.where_expr, "ts < 100");
11700            }
11701            other => panic!("expected Purge, got {other:?}"),
11702        }
11703    }
11704
11705    #[test]
11706    fn mutate_without_a_where_block_is_a_whole_store_op() {
11707        // No `{ where: }` → an empty filter → the runtime renders
11708        // `WHERE TRUE` (every row). A valid, intentional op.
11709        let prog = parse("flow F() -> Unit { mutate accounts }");
11710        match first_step(&prog, "F") {
11711            FlowStep::Mutate(m) => {
11712                assert_eq!(m.store_name, "accounts");
11713                assert_eq!(m.where_expr, "");
11714            }
11715            other => panic!("expected Mutate, got {other:?}"),
11716        }
11717    }
11718}
11719
11720// ── §Fase 35.o — persist field-block capture ────────────────────────
11721
11722#[cfg(test)]
11723mod fase35o_persist_fields_tests {
11724    use super::*;
11725
11726    fn parse(src: &str) -> Program {
11727        let tokens = crate::lexer::Lexer::new(src, "<test>")
11728            .tokenize()
11729            .expect("lex");
11730        Parser::new(tokens).parse().expect("parse")
11731    }
11732
11733    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
11734        for d in &prog.declarations {
11735            if let Declaration::Flow(f) = d {
11736                if f.name == flow {
11737                    return f.body.first().expect("flow has at least one step");
11738                }
11739            }
11740        }
11741        panic!("flow `{flow}` not found");
11742    }
11743
11744    #[test]
11745    fn persist_captures_its_field_block() {
11746        // Pre-35.o the `{ col: value }` block was skipped — every
11747        // persist wrote the whole binding context. It must now reach
11748        // `fields`, in source order, with value expressions raw.
11749        let prog = parse(
11750            "flow F() -> Unit { persist into chat_history { \
11751             session_id: \"${session_id}\" sender: \"user\" \
11752             content: \"${message}\" } }",
11753        );
11754        match first_step(&prog, "F") {
11755            FlowStep::Persist(p) => {
11756                assert_eq!(p.store_name, "chat_history");
11757                assert_eq!(
11758                    p.fields,
11759                    vec![
11760                        ("session_id".to_string(), "${session_id}".to_string()),
11761                        ("sender".to_string(), "user".to_string()),
11762                        ("content".to_string(), "${message}".to_string()),
11763                    ]
11764                );
11765            }
11766            other => panic!("expected Persist, got {other:?}"),
11767        }
11768    }
11769
11770    #[test]
11771    fn persist_without_a_block_keeps_the_user_bindings_fallback() {
11772        // No `{ }` → empty `fields` → the runtime falls back to the
11773        // v1.30.0 user-bindings row. Backward-compatible.
11774        let prog = parse("flow F() -> Unit { persist events }");
11775        match first_step(&prog, "F") {
11776            FlowStep::Persist(p) => {
11777                assert_eq!(p.store_name, "events");
11778                assert!(p.fields.is_empty());
11779            }
11780            other => panic!("expected Persist, got {other:?}"),
11781        }
11782    }
11783
11784    #[test]
11785    fn persist_accepts_the_optional_into_connector() {
11786        // `persist into X` and `persist X` resolve to the SAME store
11787        // name — pre-35.o `into` was captured AS the store name.
11788        let with =
11789            parse("flow F() -> Unit { persist into accounts { id: \"1\" } }");
11790        let without =
11791            parse("flow F() -> Unit { persist accounts { id: \"1\" } }");
11792        for prog in [&with, &without] {
11793            match first_step(prog, "F") {
11794                FlowStep::Persist(p) => assert_eq!(p.store_name, "accounts"),
11795                other => panic!("expected Persist, got {other:?}"),
11796            }
11797        }
11798    }
11799
11800    #[test]
11801    fn persist_into_without_a_block_resolves_the_store_name() {
11802        // `persist into events` — the `into` connector is skipped, the
11803        // store name is `events` (not `into`). Lateral bug closed.
11804        let prog = parse("flow F() -> Unit { persist into events }");
11805        match first_step(&prog, "F") {
11806            FlowStep::Persist(p) => {
11807                assert_eq!(p.store_name, "events");
11808                assert!(p.fields.is_empty());
11809            }
11810            other => panic!("expected Persist, got {other:?}"),
11811        }
11812    }
11813
11814    #[test]
11815    fn persist_fields_lower_into_the_ir() {
11816        // The IR generator must carry `fields` onto `IRPersistStep`
11817        // so the runtime reads exactly the declared columns.
11818        let prog = parse(
11819            "flow F() -> Unit { persist into chat { content: \"${msg}\" } }",
11820        );
11821        let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
11822        let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
11823        match flow.steps.first().expect("one step") {
11824            crate::ir_nodes::IRFlowNode::Persist(p) => {
11825                assert_eq!(p.store_name, "chat");
11826                assert_eq!(
11827                    p.fields,
11828                    vec![("content".to_string(), "${msg}".to_string())]
11829                );
11830            }
11831            other => panic!("expected IRFlowNode::Persist, got {other:?}"),
11832        }
11833    }
11834}
11835
11836// ── §Fase 35.p — mutate SET-field-block capture ─────────────────────
11837
11838#[cfg(test)]
11839mod fase35p_mutate_fields_tests {
11840    use super::*;
11841
11842    fn parse(src: &str) -> Program {
11843        let tokens = crate::lexer::Lexer::new(src, "<test>")
11844            .tokenize()
11845            .expect("lex");
11846        Parser::new(tokens).parse().expect("parse")
11847    }
11848
11849    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
11850        for d in &prog.declarations {
11851            if let Declaration::Flow(f) = d {
11852                if f.name == flow {
11853                    return f.body.first().expect("flow has at least one step");
11854                }
11855            }
11856        }
11857        panic!("flow `{flow}` not found");
11858    }
11859
11860    #[test]
11861    fn mutate_captures_its_set_field_block() {
11862        // Pre-35.p every key but `where:` was skipped — the runtime
11863        // SET every flow binding. The SET columns must now reach
11864        // `fields`, in source order, with `where:` still captured.
11865        let prog = parse(
11866            "flow F() -> Unit { mutate accounts { where: \"id = ${id}\" \
11867             balance: \"${new_balance}\" status: \"active\" } }",
11868        );
11869        match first_step(&prog, "F") {
11870            FlowStep::Mutate(m) => {
11871                assert_eq!(m.store_name, "accounts");
11872                assert_eq!(m.where_expr, "id = ${id}");
11873                assert_eq!(
11874                    m.fields,
11875                    vec![
11876                        ("balance".to_string(), "${new_balance}".to_string()),
11877                        ("status".to_string(), "active".to_string()),
11878                    ]
11879                );
11880            }
11881            other => panic!("expected Mutate, got {other:?}"),
11882        }
11883    }
11884
11885    #[test]
11886    fn mutate_where_only_block_has_no_set_fields() {
11887        // A `{ where: }`-only block → empty `fields` → the runtime
11888        // falls back to the v1.31.0 user-bindings SET.
11889        let prog =
11890            parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
11891        match first_step(&prog, "F") {
11892            FlowStep::Mutate(m) => {
11893                assert_eq!(m.where_expr, "id = 1");
11894                assert!(m.fields.is_empty());
11895            }
11896            other => panic!("expected Mutate, got {other:?}"),
11897        }
11898    }
11899
11900    #[test]
11901    fn mutate_with_no_block_is_a_whole_store_op() {
11902        // No block at all → empty where + empty fields (a whole-store
11903        // UPDATE from user bindings) — unchanged from 35.m.
11904        let prog = parse("flow F() -> Unit { mutate accounts }");
11905        match first_step(&prog, "F") {
11906            FlowStep::Mutate(m) => {
11907                assert_eq!(m.store_name, "accounts");
11908                assert_eq!(m.where_expr, "");
11909                assert!(m.fields.is_empty());
11910            }
11911            other => panic!("expected Mutate, got {other:?}"),
11912        }
11913    }
11914
11915    #[test]
11916    fn mutate_fields_lower_into_the_ir() {
11917        let prog = parse(
11918            "flow F() -> Unit { mutate t { where: \"id = 1\" v: \"${x}\" } }",
11919        );
11920        let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
11921        let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
11922        match flow.steps.first().expect("one step") {
11923            crate::ir_nodes::IRFlowNode::Mutate(m) => {
11924                assert_eq!(m.where_expr, "id = 1");
11925                assert_eq!(
11926                    m.fields,
11927                    vec![("v".to_string(), "${x}".to_string())]
11928                );
11929            }
11930            other => panic!("expected IRFlowNode::Mutate, got {other:?}"),
11931        }
11932    }
11933}
11934