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        // §Fase 120 — an `effect` declaration carries its comments like any peer.
80        Declaration::Effect(n) => {
81            n.leading_trivia = leading;
82            n.trailing_trivia = trailing;
83        }
84        Declaration::Tool(n) => {
85            n.leading_trivia = leading;
86            n.trailing_trivia = trailing;
87        }
88        Declaration::Type(n) => {
89            n.leading_trivia = leading;
90            n.trailing_trivia = trailing;
91        }
92        Declaration::Flow(n) => {
93            n.leading_trivia = leading;
94            n.trailing_trivia = trailing;
95        }
96        Declaration::Intent(n) => {
97            n.leading_trivia = leading;
98            n.trailing_trivia = trailing;
99        }
100        Declaration::Run(n) => {
101            n.leading_trivia = leading;
102            n.trailing_trivia = trailing;
103        }
104        Declaration::Epistemic(n) => {
105            n.leading_trivia = leading;
106            n.trailing_trivia = trailing;
107        }
108        Declaration::Let(n) => {
109            n.leading_trivia = leading;
110            n.trailing_trivia = trailing;
111        }
112        Declaration::LambdaData(n) => {
113            n.leading_trivia = leading;
114            n.trailing_trivia = trailing;
115        }
116        Declaration::Agent(n) => {
117            n.leading_trivia = leading;
118            n.trailing_trivia = trailing;
119        }
120        Declaration::Shield(n) => {
121            n.leading_trivia = leading;
122            n.trailing_trivia = trailing;
123        }
124        Declaration::Window(n) => {
125            n.leading_trivia = leading;
126            n.trailing_trivia = trailing;
127        }
128        Declaration::Pix(n) => {
129            n.leading_trivia = leading;
130            n.trailing_trivia = trailing;
131        }
132        Declaration::Ledger(n) => {
133            n.leading_trivia = leading;
134            n.trailing_trivia = trailing;
135        }
136        Declaration::Psyche(n) => {
137            n.leading_trivia = leading;
138            n.trailing_trivia = trailing;
139        }
140        Declaration::Corpus(n) => {
141            n.leading_trivia = leading;
142            n.trailing_trivia = trailing;
143        }
144        Declaration::Dataspace(n) => {
145            n.leading_trivia = leading;
146            n.trailing_trivia = trailing;
147        }
148        Declaration::Ots(n) => {
149            n.leading_trivia = leading;
150            n.trailing_trivia = trailing;
151        }
152        Declaration::Mandate(n) => {
153            n.leading_trivia = leading;
154            n.trailing_trivia = trailing;
155        }
156        Declaration::Compute(n) => {
157            n.leading_trivia = leading;
158            n.trailing_trivia = trailing;
159        }
160        Declaration::Daemon(n) => {
161            n.leading_trivia = leading;
162            n.trailing_trivia = trailing;
163        }
164        Declaration::Extension(n) => {
165            n.leading_trivia = leading;
166            n.trailing_trivia = trailing;
167        }
168        Declaration::AxonStore(n) => {
169            n.leading_trivia = leading;
170            n.trailing_trivia = trailing;
171        }
172        Declaration::AxonEndpoint(n) => {
173            n.leading_trivia = leading;
174            n.trailing_trivia = trailing;
175        }
176        Declaration::Resource(n) => {
177            n.leading_trivia = leading;
178            n.trailing_trivia = trailing;
179        }
180        Declaration::Fabric(n) => {
181            n.leading_trivia = leading;
182            n.trailing_trivia = trailing;
183        }
184        Declaration::Manifest(n) => {
185            n.leading_trivia = leading;
186            n.trailing_trivia = trailing;
187        }
188        Declaration::Observe(n) => {
189            n.leading_trivia = leading;
190            n.trailing_trivia = trailing;
191        }
192        Declaration::Reconcile(n) => {
193            n.leading_trivia = leading;
194            n.trailing_trivia = trailing;
195        }
196        Declaration::Lease(n) => {
197            n.leading_trivia = leading;
198            n.trailing_trivia = trailing;
199        }
200        Declaration::Ensemble(n) => {
201            n.leading_trivia = leading;
202            n.trailing_trivia = trailing;
203        }
204        Declaration::Session(n) => {
205            n.leading_trivia = leading;
206            n.trailing_trivia = trailing;
207        }
208        Declaration::Topology(n) => {
209            n.leading_trivia = leading;
210            n.trailing_trivia = trailing;
211        }
212        Declaration::Immune(n) => {
213            n.leading_trivia = leading;
214            n.trailing_trivia = trailing;
215        }
216        Declaration::Reflex(n) => {
217            n.leading_trivia = leading;
218            n.trailing_trivia = trailing;
219        }
220        Declaration::Heal(n) => {
221            n.leading_trivia = leading;
222            n.trailing_trivia = trailing;
223        }
224        Declaration::Component(n) => {
225            n.leading_trivia = leading;
226            n.trailing_trivia = trailing;
227        }
228        Declaration::View(n) => {
229            n.leading_trivia = leading;
230            n.trailing_trivia = trailing;
231        }
232        Declaration::Channel(n) => {
233            n.leading_trivia = leading;
234            n.trailing_trivia = trailing;
235        }
236        Declaration::Socket(n) => {
237            n.leading_trivia = leading;
238            n.trailing_trivia = trailing;
239        }
240        Declaration::Upstream(n) => {
241            n.leading_trivia = leading;
242            n.trailing_trivia = trailing;
243        }
244        Declaration::Voice(n) => {
245            n.leading_trivia = leading;
246            n.trailing_trivia = trailing;
247        }
248        Declaration::Cors(n) => {
249            n.leading_trivia = leading;
250            n.trailing_trivia = trailing;
251        }
252        Declaration::Credential(n) => {
253            n.leading_trivia = leading;
254            n.trailing_trivia = trailing;
255        }
256        Declaration::Cache(n) => {
257            n.leading_trivia = leading;
258            n.trailing_trivia = trailing;
259        }
260        Declaration::Savant(n) => {
261            n.leading_trivia = leading;
262            n.trailing_trivia = trailing;
263        }
264        Declaration::Synth(n) => {
265            n.leading_trivia = leading;
266            n.trailing_trivia = trailing;
267        }
268        Declaration::Scope(n) => {
269            n.leading_trivia = leading;
270            n.trailing_trivia = trailing;
271        }
272        Declaration::Observable(n) => {
273            n.leading_trivia = leading;
274            n.trailing_trivia = trailing;
275        }
276        Declaration::Witness(n) => {
277            n.leading_trivia = leading;
278            n.trailing_trivia = trailing;
279        }
280        Declaration::Document(n) => {
281            n.leading_trivia = leading;
282            n.trailing_trivia = trailing;
283        }
284        Declaration::Deliver(n) => {
285            n.leading_trivia = leading;
286            n.trailing_trivia = trailing;
287        }
288        Declaration::Notify(n) => {
289            n.leading_trivia = leading;
290            n.trailing_trivia = trailing;
291        }
292        Declaration::Generic(n) => {
293            n.leading_trivia = leading;
294            n.trailing_trivia = trailing;
295        }
296    }
297}
298
299// ── Public error type ────────────────────────────────────────────────────────
300
301/// §Fase 28.d — Source-context constants. D4 ratified 2026-05-10:
302/// 2 lines before + 2 lines after the error line. Mirror of the
303/// Python-side `_SOURCE_CONTEXT_LINES_BEFORE` / `_AFTER` so the
304/// rustc-style block has identical shape across stacks.
305pub const SOURCE_CONTEXT_LINES_BEFORE: usize = 2;
306pub const SOURCE_CONTEXT_LINES_AFTER: usize = 2;
307
308/// §Fase 28.d — Rustc-style source-context block for a parse error.
309///
310/// Holds a reference to the source text plus the line/column the
311/// error points at. Rendering is lazy — call ``render()`` to format
312/// the block (line numbers + caret + 2 lines before + 2 after).
313///
314/// Pure and deterministic: no ANSI colors, no terminal-width
315/// detection. Output shape is byte-identical to the Python
316/// `SourceSnippet.render()` on the same input — that's the cross-
317/// stack drift gate (28.i).
318#[derive(Debug, Clone)]
319pub struct SourceSnippet {
320    pub source: String,
321    pub line: u32,
322    pub column: u32,
323    pub filename: String,
324    pub context_before: usize,
325    pub context_after: usize,
326}
327
328impl SourceSnippet {
329    /// Construct with the default 2/2 context window.
330    pub fn new(source: String, line: u32, column: u32, filename: String) -> Self {
331        Self {
332            source,
333            line,
334            column,
335            filename,
336            context_before: SOURCE_CONTEXT_LINES_BEFORE,
337            context_after: SOURCE_CONTEXT_LINES_AFTER,
338        }
339    }
340
341    /// Format the snippet as a multi-line rustc-style block.
342    ///
343    /// Empty source → empty string. Out-of-range line → empty
344    /// string. Caret column is clamped to `[1, line_len + 1]`.
345    /// Output shape matches Python `SourceSnippet.render` byte-
346    /// identically per D7.
347    #[must_use]
348    pub fn render(&self) -> String {
349        if self.source.is_empty() || self.line < 1 {
350            return String::new();
351        }
352        let raw: Vec<&str> = self.source.split('\n').collect();
353        // Match Python's str.splitlines() trailing-newline shape:
354        // strip an empty trailing entry produced by a final '\n'.
355        let lines: Vec<&str> = if raw.last() == Some(&"") {
356            raw[..raw.len() - 1].to_vec()
357        } else {
358            raw
359        };
360        if lines.is_empty() || self.line as usize > lines.len() {
361            return String::new();
362        }
363
364        let line_idx = self.line as usize;
365        let start = line_idx.saturating_sub(self.context_before).max(1);
366        let end = (line_idx + self.context_after).min(lines.len());
367
368        let gutter = end.to_string().len();
369        let empty_gutter = " ".repeat(gutter);
370
371        let mut out: Vec<String> = Vec::with_capacity(end - start + 4);
372        out.push(format!(
373            "{empty_gutter} --> {}:{}:{}",
374            self.filename, self.line, self.column
375        ));
376        out.push(format!("{empty_gutter} |"));
377        for n in start..=end {
378            let line_text = lines[n - 1];
379            out.push(format!("{n:>gutter$} | {line_text}", gutter = gutter));
380            if n == line_idx {
381                let line_len = line_text.chars().count();
382                let col = (self.column as usize).clamp(1, line_len + 1);
383                out.push(format!(
384                    "{empty_gutter} | {pad}^",
385                    pad = " ".repeat(col - 1)
386                ));
387            }
388        }
389        out.join("\n")
390    }
391}
392
393#[derive(Debug, Clone, Default)]
394pub struct ParseError {
395    pub message: String,
396    pub line: u32,
397    pub column: u32,
398    /// §Fase 28.d — Optional rustc-style source-context block.
399    /// `None` preserves the legacy single-line shape; populated by
400    /// `Parser::with_source` callers (and by `parse_with_recovery`
401    /// / `parse` when a source has been attached to the parser).
402    /// Existing struct-literal call sites use the `..Default::default()`
403    /// idiom (default = None) to stay terse.
404    pub source_snippet: Option<SourceSnippet>,
405}
406
407impl ParseError {
408    /// §Fase 28.d — Attach a `SourceSnippet` derived from raw source
409    /// text and filename. Returns `self` so the call can be chained
410    /// at the construction site. No-op when `line == 0`. Idempotent.
411    #[must_use]
412    pub fn attach_source(mut self, source: &str, filename: &str) -> Self {
413        if self.line >= 1 {
414            self.source_snippet = Some(SourceSnippet::new(
415                source.to_string(),
416                self.line,
417                self.column,
418                filename.to_string(),
419            ));
420        }
421        self
422    }
423}
424
425impl std::fmt::Display for ParseError {
426    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
427        write!(f, "[line {}:{}] {}", self.line, self.column, self.message)?;
428        if let Some(snippet) = &self.source_snippet {
429            let block = snippet.render();
430            if !block.is_empty() {
431                write!(f, "\n{block}")?;
432            }
433        }
434        Ok(())
435    }
436}
437
438impl std::error::Error for ParseError {}
439
440// ── §Fase 28.c — Public recovery result ──────────────────────────────────────
441//
442// Mirror of Python's `axon.compiler.parser.ParseResult` (Fase 28.b).
443// The rationale, sync semantics, and test contract are documented in
444// `docs/fase/fase_28_adopter_diagnostic_robustness.md`. The Rust frontend
445// must produce structurally identical error lists to the Python parser
446// when handed the same source — that is the cross-stack drift gate
447// (D7 ratified 2026-05-10: byte-identical error lists).
448//
449// `program` holds whatever declarations the parser was able to parse
450// successfully. `errors` holds every recovered error in source order.
451// A clean parse returns `errors.is_empty()`; the existing fail-fast
452// `parse()` API is preserved verbatim per D9.
453
454/// Outcome of `Parser::parse_with_recovery` — partial program plus the
455/// list of every error the parser recovered from. See module docs for
456/// the panic-mode + sync-point recovery semantics.
457#[derive(Debug)]
458pub struct ParseResult {
459    pub program: Program,
460    pub errors: Vec<ParseError>,
461}
462
463impl ParseResult {
464    /// True iff at least one parse error was recovered. Callers that
465    /// want to short-circuit on failure should check this rather than
466    /// relying on `program.declarations.is_empty()` (the parser may
467    /// have salvaged some declarations even with errors present).
468    #[inline]
469    #[must_use]
470    pub fn has_errors(&self) -> bool {
471        !self.errors.is_empty()
472    }
473
474    /// Inverse of `has_errors`. Convenience for the "happy path" check
475    /// in tests + adopter integrations.
476    #[inline]
477    #[must_use]
478    pub fn is_clean(&self) -> bool {
479        self.errors.is_empty()
480    }
481}
482
483/// §Fase 28.c — Top-level declaration keywords used as resync points
484/// during error recovery (D2 ratified 2026-05-10). Mirrors the
485/// `_TOP_LEVEL_DECLARATION_KEYWORDS` frozenset on the Python side.
486///
487/// Distinct from `tokens::is_declaration_keyword` because that helper
488/// is used by the structural declaration counter and intentionally
489/// excludes some grammar-only tokens (Know/Believe/Speculate/Doubt,
490/// Ingest, Ots) that DO begin a top-level declaration in
491/// `parse_declaration` and therefore must be valid sync points.
492///
493/// Adding a new top-level dispatch arm in `parse_declaration` MUST
494/// add the corresponding token here so the recovery walker can
495/// re-sync correctly.
496#[inline]
497const fn is_top_level_decl_kw_for_recovery(tt: &TokenType) -> bool {
498    matches!(
499        tt,
500        TokenType::Import
501            | TokenType::Persona
502            | TokenType::Context
503            | TokenType::Anchor
504            | TokenType::Memory
505            | TokenType::Tool
506            | TokenType::Type
507            | TokenType::Flow
508            | TokenType::Intent
509            | TokenType::Run
510            | TokenType::Let
511            | TokenType::Know
512            | TokenType::Believe
513            | TokenType::Speculate
514            | TokenType::Doubt
515            | TokenType::Lambda
516            | TokenType::Agent
517            | TokenType::Shield
518            | TokenType::Pix
519            | TokenType::Ledger
520            | TokenType::Psyche
521            | TokenType::Corpus
522            | TokenType::Dataspace
523            | TokenType::Ots
524            | TokenType::Mandate
525            | TokenType::Compute
526            | TokenType::Daemon
527            // §Fase 87.a/d — the autonomous research primitive + synth policy.
528            | TokenType::Savant
529            | TokenType::Synth
530            // §Fase 88.a — the authorization-scope policy declaration.
531            | TokenType::Scope
532            | TokenType::AxonStore
533            | TokenType::AxonEndpoint
534            | TokenType::Resource
535            | TokenType::Fabric
536            | TokenType::Manifest
537            | TokenType::Observe
538            | TokenType::Reconcile
539            | TokenType::Lease
540            | TokenType::Ensemble
541            | TokenType::Session
542            | TokenType::Topology
543            | TokenType::Immune
544            | TokenType::Reflex
545            | TokenType::Heal
546            | TokenType::Component
547            | TokenType::View
548            | TokenType::Channel
549            | TokenType::Ingest
550            | TokenType::Persist
551            | TokenType::Retrieve
552            | TokenType::Mutate
553            | TokenType::Purge
554            | TokenType::Transact
555            | TokenType::Mcp
556    )
557}
558
559// ── §Fase 30.b — axonendpoint transport + keepalive closed enums ────────────
560//
561// D2 ratified 2026-05-10: `transport` is a closed enum
562// {json, sse, ndjson}. D6 ratified: `keepalive` is a closed enum
563// {5s, 15s, 30s, 60s}. Both mirror the Python frontend's
564// `_AXONENDPOINT_TRANSPORT_VALUES` / `_AXONENDPOINT_KEEPALIVE_VALUES`
565// frozensets in `axon/compiler/parser.py`. Cross-stack drift gate
566// (30.b fixture) asserts byte-identical parse for every entry.
567
568/// Adopter-facing acceptable values for `transport:` field.
569/// Used by both the parser (validation + smart-suggest) and the
570/// type-checker (30.c) so adopter tooling sees one canonical list.
571pub const AXONENDPOINT_TRANSPORT_VALUES: &[&str] = &["json", "sse", "ndjson"];
572
573/// §Fase 33.z.k.b (v1.28.0) — Closed-catalog SSE wire-format
574/// dialects. Selected via the parametrized grammar
575/// `transport: sse(<dialect>)`; bare `transport: sse` resolves to
576/// the Q1 default per the flow's algebraic-effect predicate
577/// (openai for tool-streaming flows; axon for type-annotation-only).
578///
579/// Vertical-grounded scope (Q3 revised 2026-05-14): five dialects
580/// cover ~99% of LLM-streaming adopter expectations.
581///   - `axon`      — current W3C named events
582///                   (event: axon.token / event: axon.complete).
583///                   D6 backwards-compat baseline; indefinitely
584///                   supported as a first-class option.
585///   - `openai`    — `data: {"choices":[{"delta":{...}}]}` frames
586///                   terminated by `data: [DONE]`. OpenAI Chat
587///                   Completions streaming wire verbatim.
588///   - `kimi`      — Moonshot Kimi (kimi.moonshot.cn) — uses the
589///                   OpenAI-compatible Chat Completions wire format
590///                   verbatim (same chunk shape, same `data: [DONE]`
591///                   sentinel). First-class entry so adopters
592///                   declare intent explicitly; under the hood the
593///                   wire is identical to `openai`.
594///   - `glm`       — Zhipu ChatGLM (open.bigmodel.cn) — same as
595///                   kimi, uses OpenAI-compat wire. First-class
596///                   entry for adopter clarity.
597///   - `anthropic` — `event: content_block_delta` frames terminated
598///                   by `event: message_stop`. Adopter SDKs
599///                   targeting Anthropic Claude consume this shape
600///                   verbatim.
601///
602/// Why kimi + glm as first-class entries (Q3 revision rationale):
603/// The project's primary adopter pipelines through Kimi K2.x +
604/// Zhipu GLM-4.x. While the wire IS byte-identical to OpenAI's
605/// Chat Completions streaming, declaring `transport: sse(kimi)` /
606/// `transport: sse(glm)` lets the audit trail + observability
607/// surfaces correlate adopter intent against the underlying
608/// provider — without the adopter having to know that "kimi
609/// happens to be OpenAI-compat on the wire today". The runtime
610/// dispatches kimi + glm to the same `OpenAIDialectAdapter` so
611/// the wire shape stays canonical-OpenAI-bytes.
612///
613/// Open-set adapter pluggability (downstream crates registering
614/// custom dialects) remains explicitly out of scope per the
615/// Axon-for-Axon discipline.
616pub const AXONENDPOINT_TRANSPORT_DIALECTS: &[&str] =
617    &["axon", "openai", "kimi", "glm", "anthropic"];
618
619/// Adopter-facing acceptable values for `keepalive:` field.
620pub const AXONENDPOINT_KEEPALIVE_VALUES: &[&str] = &["5s", "15s", "30s", "60s"];
621
622/// §Fase 32.b D3 — Closed method enum for `method:` field. Adopter-
623/// declarable methods only; HEAD/OPTIONS/CONNECT/TRACE are
624/// runtime-managed (CORS preflight, etc.) and never declared from
625/// source. Closed enum refuses interpretation drift; smart-suggest
626/// catches near-misses at parse time.
627///
628/// §Fase 107.a — `QUERY` (RFC 10008, Proposed Standard, June 2026): the safe +
629/// idempotent + cacheable method that CARRIES A REQUEST BODY — the first new HTTP
630/// method in two decades. It carries a LAW, not just a route: `axon-T927` refuses
631/// at compile time a QUERY endpoint whose flow performs a declared write (the
632/// RFC's normative "safe and idempotent" MUST, made a proof).
633///
634/// Must stay in lockstep with `type_checker::VALID_ENDPOINT_METHODS`.
635pub const AXONENDPOINT_METHOD_VALUES: &[&str] =
636    &["GET", "POST", "PUT", "DELETE", "PATCH", "QUERY"];
637
638/// §Fase 36.d (D2) — Closed catalog for the `axonendpoint backend:`
639/// declaration. The set is `CANONICAL_PROVIDERS ∪ {auto, stub}`:
640///
641///   - the seven canonical LLM providers — `anthropic`, `gemini`,
642///     `glm`, `kimi`, `ollama`, `openai`, `openrouter` — a concrete,
643///     declared backend that rung 2 of the Fase 36 D1 resolution
644///     ladder fires immediately;
645///   - `auto` — transparent: declaring it is equivalent to omitting
646///     `backend:` entirely (the route resolves down the ladder —
647///     server default → environment-available providers);
648///   - `stub` — the no-op backend, reachable ONLY by an explicit,
649///     written declaration (D5: a silent degradation to `stub` is
650///     forbidden; an explicit opt-in is not).
651///
652/// `axon-frontend` carries zero runtime deps and therefore cannot
653/// import `axon::backends::CANONICAL_PROVIDERS`; this list is a
654/// hand-maintained mirror. The axon-rs drift gate
655/// (`tests/fase36_d_backend_catalog_drift.rs`) asserts the two stay
656/// byte-identical — adding a provider in one place without the other
657/// fails CI.
658pub const AXONENDPOINT_BACKEND_VALUES: &[&str] = &[
659    "anthropic",
660    "auto",
661    "gemini",
662    "glm",
663    "kimi",
664    "ollama",
665    "openai",
666    "openrouter",
667    "stub",
668];
669
670#[inline]
671fn axonendpoint_is_valid_transport(s: &str) -> bool {
672    AXONENDPOINT_TRANSPORT_VALUES.iter().any(|&v| v == s)
673}
674
675#[inline]
676fn axonendpoint_is_valid_method(s: &str) -> bool {
677    AXONENDPOINT_METHOD_VALUES.iter().any(|&v| v == s)
678}
679
680#[inline]
681fn axonendpoint_is_valid_backend(s: &str) -> bool {
682    AXONENDPOINT_BACKEND_VALUES.iter().any(|&v| v == s)
683}
684
685#[inline]
686fn axonendpoint_is_valid_keepalive(s: &str) -> bool {
687    AXONENDPOINT_KEEPALIVE_VALUES.iter().any(|&v| v == s)
688}
689
690/// §Fase 37.y (D2) — Closed type catalog for query parameters.
691///
692/// Query values arrive over HTTP as URL-encoded strings; the catalog
693/// is the set of types axon will validate / coerce them into for the
694/// Request Binding Contract. Hand-curated, intentionally small:
695///   - `Text` — the raw string (always succeeds)
696///   - `Int` — `i64` parseable
697///   - `Float` — `f64` parseable, finite
698///   - `Bool` — case-insensitive `{true, false, 1, 0, yes, no, on, off}`
699///   - `Uuid` — RFC 4122 textual form
700///
701/// Extending the catalog is a future axon-T?nn surface; v1.38.5 ships
702/// the 5 types covering ~95% of REST query patterns. Lists / dates /
703/// datetimes / enums are honest deferrals (see §7 of the plan vivo).
704pub const AXONENDPOINT_QUERY_PARAM_TYPES: &[&str] =
705    &["Text", "Int", "Float", "Bool", "Uuid"];
706
707/// `true` iff `s` is one of the §Fase 37.y (D2) query-param catalog
708/// entries — exact case-sensitive match (axon types are PascalCase).
709#[inline]
710pub(crate) fn axonendpoint_is_valid_query_param_type(s: &str) -> bool {
711    AXONENDPOINT_QUERY_PARAM_TYPES.iter().any(|&v| v == s)
712}
713
714/// §Fase 37.y (D1) — Extract `{name}` placeholder names from an
715/// `axonendpoint` `path:` string, in left-to-right declaration order.
716///
717/// Recognized placeholder grammar (single-segment, no nested braces):
718/// `{NAME}` where `NAME` matches `[A-Za-z_][A-Za-z0-9_]*`. Anything
719/// inside braces that does NOT match the identifier shape is silently
720/// IGNORED — it's either an adopter typo (caught later by axum at
721/// route registration) or a literal brace in the URL pattern.
722///
723/// Returns `Err(duplicate_name)` when the same `{name}` appears more
724/// than once in the path — HTTP route patterns reject duplicates
725/// structurally (`axum` would panic at registration), so surfacing
726/// the error at parse time is the right place.
727///
728/// Pure + total: never panics; deterministic over its single string
729/// argument. Hand-rolled scanner (no regex dep at parser layer).
730///
731/// # Examples
732///
733/// - `"/api/users"` → `Ok(vec![])`
734/// - `"/api/users/{id}"` → `Ok(vec!["id"])`
735/// - `"/api/tenants/{tenant_id}/secrets/{secret_name}"`
736///   → `Ok(vec!["tenant_id", "secret_name"])`
737/// - `"/api/users/{id}/posts/{id}"` → `Err("id")` (duplicate)
738/// - `"/api/{not valid}"` → `Ok(vec![])` (malformed brace content
739///   silently ignored; axum surfaces the error at registration)
740pub(crate) fn extract_path_param_names(path: &str) -> Result<Vec<String>, String> {
741    let mut out: Vec<String> = Vec::new();
742    let bytes = path.as_bytes();
743    let mut i = 0;
744    while i < bytes.len() {
745        if bytes[i] != b'{' {
746            i += 1;
747            continue;
748        }
749        // Find the matching close brace; if none, the open brace is
750        // a literal — leave it alone.
751        let start = i + 1;
752        let mut end = start;
753        while end < bytes.len() && bytes[end] != b'}' {
754            end += 1;
755        }
756        if end == bytes.len() {
757            // Unterminated — give up; downstream parser/runtime
758            // surface the malformed path elsewhere.
759            break;
760        }
761        let raw = &path[start..end];
762        // Validate identifier shape: [A-Za-z_][A-Za-z0-9_]*
763        let valid = !raw.is_empty()
764            && raw.bytes().enumerate().all(|(idx, b)| {
765                if idx == 0 {
766                    b.is_ascii_alphabetic() || b == b'_'
767                } else {
768                    b.is_ascii_alphanumeric() || b == b'_'
769                }
770            });
771        if valid {
772            let name = raw.to_string();
773            if out.iter().any(|existing| existing == &name) {
774                return Err(name);
775            }
776            out.push(name);
777        }
778        i = end + 1;
779    }
780    Ok(out)
781}
782
783/// §Fase 32.g (D8) — Closed capability-slug grammar. Validates a
784/// `requires:` slug per `^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$`.
785///
786/// Hand-rolled (no regex dep at parser layer) — each segment must
787/// match `[a-z][a-z0-9_]*` and segments are joined by single dots.
788/// Public so the runtime mirror (`axon::auth_scope`) reuses the same
789/// predicate without duplicating the rule.
790///
791/// Examples valid: `admin`, `legal.read`, `hipaa.phi.read`,
792/// `bank.officer.senior`, `a`, `a_b`, `a1`.
793/// Examples invalid: empty, `Admin` (uppercase), `1admin` (digit
794/// first), `bank-officer` (hyphen), `bank..a` (empty segment),
795/// `.admin`, `admin.`, `admin..` .
796pub fn is_valid_capability_slug(slug: &str) -> bool {
797    if slug.is_empty() {
798        return false;
799    }
800    for segment in slug.split('.') {
801        if !is_valid_slug_segment(segment) {
802            return false;
803        }
804    }
805    true
806}
807
808fn is_valid_slug_segment(seg: &str) -> bool {
809    let mut chars = seg.chars();
810    let first = match chars.next() {
811        Some(c) => c,
812        None => return false,
813    };
814    if !first.is_ascii_lowercase() {
815        return false;
816    }
817    chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
818}
819
820// ════════════════════════════════════════════════════════════════════
821//  §Fase 37.y (D1) — `extract_path_param_names` unit tests
822// ════════════════════════════════════════════════════════════════════
823
824// ════════════════════════════════════════════════════════════════════
825//  §Fase 37.y (D2) — `axonendpoint_is_valid_query_param_type` + the
826//  inline `query: { … }` parser, end-to-end through the lexer.
827// ════════════════════════════════════════════════════════════════════
828
829#[cfg(test)]
830mod query_param_catalog_tests {
831    use super::{axonendpoint_is_valid_query_param_type, AXONENDPOINT_QUERY_PARAM_TYPES};
832
833    #[test]
834    fn accepts_every_catalog_entry() {
835        for ty in AXONENDPOINT_QUERY_PARAM_TYPES {
836            assert!(
837                axonendpoint_is_valid_query_param_type(ty),
838                "catalog entry `{ty}` must validate"
839            );
840        }
841    }
842
843    #[test]
844    fn rejects_off_catalog_types() {
845        for off in &[
846            "Timestamp",    // not in v1.38.5 — list/dates deferred
847            "Date",
848            "DateTime",
849            "List<Text>",   // multi-value query params deferred (§7)
850            "Jsonb",        // store-only types not query-applicable
851            "Bytea",
852            "text",         // lowercase rejected (axon types are PascalCase)
853            "TEXT",
854            "Number",       // not in axon's type catalog at all
855            "",             // empty
856            " ",            // whitespace
857        ] {
858            assert!(
859                !axonendpoint_is_valid_query_param_type(off),
860                "off-catalog `{off}` must reject"
861            );
862        }
863    }
864
865    #[test]
866    fn catalog_size_matches_design() {
867        // The plan vivo D2 states a closed 5-type catalog. A future
868        // axon-T?nn surface may extend it; that requires updating BOTH
869        // the catalog AND the plan vivo §7 honest-scope note.
870        assert_eq!(AXONENDPOINT_QUERY_PARAM_TYPES.len(), 5);
871    }
872}
873
874#[cfg(test)]
875mod query_param_parser_tests {
876    use crate::lexer::Lexer;
877    use crate::parser::Parser;
878
879    fn parse_endpoint_source(src: &str) -> Result<crate::ast::AxonEndpointDefinition, String> {
880        let tokens = Lexer::new(src, "test.axon")
881            .tokenize()
882            .map_err(|e| format!("lex: {}", e.message))?;
883        let mut parser = Parser::new(tokens);
884        let program = parser.parse().map_err(|e| format!("parse: {}", e.message))?;
885        program
886            .declarations
887            .into_iter()
888            .find_map(|d| match d {
889                crate::ast::Declaration::AxonEndpoint(e) => Some(e),
890                _ => None,
891            })
892            .ok_or_else(|| "no axonendpoint in program".to_string())
893    }
894
895    #[test]
896    fn endpoint_with_no_query_block_keeps_empty_vec() {
897        let src = r#"
898            axonendpoint write_secret {
899                method: POST
900                path: "/api/users"
901                body: SecretWriteRequest
902                execute: WriteSecret
903            }
904        "#;
905        let ep = parse_endpoint_source(src).expect("parses");
906        assert!(
907            ep.query_params.is_empty(),
908            "D5 — no `query:` block ⇒ empty query_params"
909        );
910    }
911
912    #[test]
913    fn single_query_param_required() {
914        let src = r#"
915            axonendpoint list_users {
916                method: GET
917                path: "/api/users"
918                query: { status: Text }
919                execute: ListUsers
920            }
921        "#;
922        let ep = parse_endpoint_source(src).expect("parses");
923        assert_eq!(ep.query_params.len(), 1);
924        assert_eq!(ep.query_params[0].name, "status");
925        assert_eq!(ep.query_params[0].type_expr.name, "Text");
926        assert!(!ep.query_params[0].type_expr.optional);
927    }
928
929    #[test]
930    fn optional_query_param_via_question_suffix() {
931        let src = r#"
932            axonendpoint list_users {
933                method: GET
934                path: "/api/users"
935                query: { limit: Int? }
936                execute: ListUsers
937            }
938        "#;
939        let ep = parse_endpoint_source(src).expect("parses");
940        assert_eq!(ep.query_params.len(), 1);
941        assert_eq!(ep.query_params[0].name, "limit");
942        assert_eq!(ep.query_params[0].type_expr.name, "Int");
943        assert!(
944            ep.query_params[0].type_expr.optional,
945            "`?` suffix sets optional"
946        );
947    }
948
949    #[test]
950    fn multiple_query_params_preserve_declaration_order() {
951        let src = r#"
952            axonendpoint search {
953                method: GET
954                path: "/api/search"
955                query: { q: Text, page: Int?, limit: Int?, exact: Bool? }
956                execute: Search
957            }
958        "#;
959        let ep = parse_endpoint_source(src).expect("parses");
960        let names: Vec<&str> = ep.query_params.iter().map(|f| f.name.as_str()).collect();
961        assert_eq!(names, vec!["q", "page", "limit", "exact"]);
962        let types: Vec<&str> = ep
963            .query_params
964            .iter()
965            .map(|f| f.type_expr.name.as_str())
966            .collect();
967        assert_eq!(types, vec!["Text", "Int", "Int", "Bool"]);
968        let optionals: Vec<bool> = ep
969            .query_params
970            .iter()
971            .map(|f| f.type_expr.optional)
972            .collect();
973        assert_eq!(optionals, vec![false, true, true, true]);
974    }
975
976    #[test]
977    fn duplicate_query_param_is_parse_error() {
978        let src = r#"
979            axonendpoint bad {
980                method: GET
981                path: "/api/x"
982                query: { name: Text, name: Int? }
983                execute: Bad
984            }
985        "#;
986        let err = parse_endpoint_source(src).expect_err("must fail");
987        assert!(
988            err.contains("duplicate query param 'name'"),
989            "error must name the duplicate. Got: {err}"
990        );
991    }
992
993    #[test]
994    fn off_catalog_type_with_smart_suggest_hint() {
995        // `Strng` is one edit away from `Text` (would suggest `Text`?
996        // Actually edit distance to `Text` is 4; to `Int` is 5. Likely
997        // no smart suggestion within distance 2. The error still names
998        // the catalog explicitly.)
999        let src = r#"
1000            axonendpoint bad {
1001                method: GET
1002                path: "/api/x"
1003                query: { value: Strng }
1004                execute: Bad
1005            }
1006        "#;
1007        let err = parse_endpoint_source(src).expect_err("must fail");
1008        assert!(
1009            err.contains("unsupported type 'Strng'"),
1010            "error must name the bad type. Got: {err}"
1011        );
1012        assert!(
1013            err.contains("Expected one of: Text | Int | Float | Bool | Uuid"),
1014            "error must list the closed catalog. Got: {err}"
1015        );
1016    }
1017
1018    #[test]
1019    fn close_typo_gets_did_you_mean_hint() {
1020        // `Txt` → edit distance 1 from `Text` → smart-suggest should
1021        // surface the hint.
1022        let src = r#"
1023            axonendpoint bad {
1024                method: GET
1025                path: "/api/x"
1026                query: { value: Txt }
1027                execute: Bad
1028            }
1029        "#;
1030        let err = parse_endpoint_source(src).expect_err("must fail");
1031        assert!(
1032            err.contains("Did you mean") && err.contains("`Text`"),
1033            "smart-suggest must hint `Text`. Got: {err}"
1034        );
1035    }
1036
1037    #[test]
1038    fn every_catalog_type_parses_cleanly() {
1039        // Round-trip smoke for all 5 catalog entries.
1040        for ty in &["Text", "Int", "Float", "Bool", "Uuid"] {
1041            let src = format!(
1042                r#"
1043                    axonendpoint x {{
1044                        method: GET
1045                        path: "/api/x"
1046                        query: {{ v: {ty} }}
1047                        execute: X
1048                    }}
1049                "#
1050            );
1051            let ep = parse_endpoint_source(&src)
1052                .unwrap_or_else(|e| panic!("`{ty}` should parse: {e}"));
1053            assert_eq!(ep.query_params[0].type_expr.name, *ty);
1054        }
1055    }
1056
1057    #[test]
1058    fn comma_optional_between_params() {
1059        // The plan vivo design accepts both comma-separated and
1060        // whitespace-separated query params (existing parser style is
1061        // forgiving). Whitespace-only:
1062        let src = r#"
1063            axonendpoint x {
1064                method: GET
1065                path: "/api/x"
1066                query: { a: Text b: Int? }
1067                execute: X
1068            }
1069        "#;
1070        let ep = parse_endpoint_source(src).expect("parses without commas");
1071        assert_eq!(ep.query_params.len(), 2);
1072    }
1073
1074    // ─── Robustness hardening (37.y.2 100% robust closure) ──────────
1075
1076    #[test]
1077    fn double_query_block_is_parse_error() {
1078        // An adopter who copy-pastes the `query:` block twice should
1079        // see a clear parse error, not a silent merge that produces
1080        // an unexpectedly-augmented endpoint with both blocks fused.
1081        let src = r#"
1082            axonendpoint x {
1083                method: GET
1084                path: "/api/x"
1085                query: { a: Text }
1086                query: { b: Int? }
1087                execute: X
1088            }
1089        "#;
1090        let err = parse_endpoint_source(src).expect_err("must fail");
1091        assert!(
1092            err.contains("declares `query: { … }` more than once"),
1093            "error must call out the duplicate block. Got: {err}"
1094        );
1095        assert!(
1096            err.contains("combine all params into a single block"),
1097            "error must hint the canonical fix. Got: {err}"
1098        );
1099    }
1100
1101    #[test]
1102    fn optional_generic_type_is_parse_error_with_canonical_hint() {
1103        // `Optional<Text>` is the wrong way to declare an optional
1104        // query param. The canonical syntax is `Text?` (the `?`
1105        // suffix). The error must surface this with a literal example.
1106        let src = r#"
1107            axonendpoint x {
1108                method: GET
1109                path: "/api/x"
1110                query: { value: Optional<Text> }
1111                execute: X
1112            }
1113        "#;
1114        let err = parse_endpoint_source(src).expect_err("must fail");
1115        assert!(
1116            err.contains("generic type `Optional<Text>`"),
1117            "error must name the generic type literally. Got: {err}"
1118        );
1119        assert!(
1120            err.contains("Use `Text?` (the `?` suffix)"),
1121            "error must hint the canonical `Text?` syntax. Got: {err}"
1122        );
1123    }
1124
1125    #[test]
1126    fn list_generic_type_is_parse_error_with_deferral_hint() {
1127        // Multi-value query params (`?tag=a&tag=b`) are honest-
1128        // deferred per the plan vivo §7. Adopters who write
1129        // `List<Text>` should see a clear error explaining the
1130        // deferral, not a confusing "type `List` not in catalog".
1131        let src = r#"
1132            axonendpoint x {
1133                method: GET
1134                path: "/api/x"
1135                query: { tags: List<Text> }
1136                execute: X
1137            }
1138        "#;
1139        let err = parse_endpoint_source(src).expect_err("must fail");
1140        assert!(
1141            err.contains("generic type `List<Text>`"),
1142            "error must name the generic type. Got: {err}"
1143        );
1144        assert!(
1145            err.contains("Multi-value query params")
1146                && err.contains("honest-deferred"),
1147            "error must mention the multi-value deferral. Got: {err}"
1148        );
1149    }
1150
1151    #[test]
1152    fn other_generic_types_caught_generically() {
1153        // Generic types beyond `Optional` and `List` get the
1154        // generic-rejection message without a canonical-syntax hint
1155        // (the catalog list is the canonical guidance).
1156        let src = r#"
1157            axonendpoint x {
1158                method: GET
1159                path: "/api/x"
1160                query: { value: Stream<Int> }
1161                execute: X
1162            }
1163        "#;
1164        let err = parse_endpoint_source(src).expect_err("must fail");
1165        assert!(
1166            err.contains("generic type `Stream<Int>`"),
1167            "error must name the generic type. Got: {err}"
1168        );
1169        assert!(
1170            err.contains("Text | Int | Float | Bool | Uuid"),
1171            "error must list the closed catalog. Got: {err}"
1172        );
1173    }
1174
1175    #[test]
1176    fn uuid_optional_parses_cleanly() {
1177        // Hardening companion — `Uuid?` is in the catalog AND
1178        // optional. The two features compose without surprise.
1179        let src = r#"
1180            axonendpoint find {
1181                method: GET
1182                path: "/api/x"
1183                query: { after: Uuid? }
1184                execute: Find
1185            }
1186        "#;
1187        let ep = parse_endpoint_source(src).expect("parses");
1188        assert_eq!(ep.query_params.len(), 1);
1189        assert_eq!(ep.query_params[0].name, "after");
1190        assert_eq!(ep.query_params[0].type_expr.name, "Uuid");
1191        assert!(ep.query_params[0].type_expr.optional);
1192        assert_eq!(ep.query_params[0].type_expr.generic_param, "");
1193    }
1194
1195    #[test]
1196    fn empty_query_block_yields_empty_vec() {
1197        // `query: { }` is grammatically valid but semantically a
1198        // no-op (equivalent to omitting the block). Don't error;
1199        // just record an empty Vec.
1200        let src = r#"
1201            axonendpoint x {
1202                method: GET
1203                path: "/api/x"
1204                query: { }
1205                execute: X
1206            }
1207        "#;
1208        let ep = parse_endpoint_source(src).expect("empty block parses");
1209        assert!(ep.query_params.is_empty());
1210    }
1211
1212    #[test]
1213    fn kivi_secret_write_path_plus_query() {
1214        // Combined path-param + query-param test: an endpoint that
1215        // takes IDs in the URL AND optional filters in the query
1216        // string. This is the natural REST shape Fase 37.y serves.
1217        let src = r#"
1218            axonendpoint write_secret {
1219                method: POST
1220                path: "/api/tenants/{tenant_id}/secrets/{secret_name}"
1221                query: { dry_run: Bool?, overwrite: Bool? }
1222                body: SecretWriteRequest
1223                execute: WriteSecret
1224            }
1225        "#;
1226        let ep = parse_endpoint_source(src).expect("parses");
1227        // Path params populated (from 37.y.1):
1228        assert_eq!(ep.path_params, vec!["tenant_id", "secret_name"]);
1229        // Query params populated (from this sub-fase 37.y.2):
1230        assert_eq!(ep.query_params.len(), 2);
1231        assert_eq!(ep.query_params[0].name, "dry_run");
1232        assert_eq!(ep.query_params[0].type_expr.name, "Bool");
1233        assert!(ep.query_params[0].type_expr.optional);
1234        assert_eq!(ep.query_params[1].name, "overwrite");
1235        // Body still works:
1236        assert_eq!(ep.body_type, "SecretWriteRequest");
1237    }
1238}
1239
1240#[cfg(test)]
1241mod path_param_extraction_tests {
1242    use super::extract_path_param_names;
1243
1244    #[test]
1245    fn empty_path_no_placeholders() {
1246        assert_eq!(extract_path_param_names("/api/users"), Ok(vec![]));
1247        assert_eq!(extract_path_param_names("/"), Ok(vec![]));
1248        assert_eq!(extract_path_param_names(""), Ok(vec![]));
1249    }
1250
1251    #[test]
1252    fn single_placeholder() {
1253        assert_eq!(
1254            extract_path_param_names("/api/users/{id}"),
1255            Ok(vec!["id".to_string()])
1256        );
1257    }
1258
1259    #[test]
1260    fn multiple_placeholders_in_declaration_order() {
1261        assert_eq!(
1262            extract_path_param_names(
1263                "/api/tenants/{tenant_id}/secrets/{secret_name}"
1264            ),
1265            Ok(vec![
1266                "tenant_id".to_string(),
1267                "secret_name".to_string(),
1268            ])
1269        );
1270    }
1271
1272    #[test]
1273    fn kivi_chat_history_path_pattern() {
1274        // The exact pattern the kivi adopter reported (2026-05-20):
1275        // POST /api/tenants/{tenant_id}/secrets/{secret_name}
1276        // Both names extracted in source order.
1277        let names = extract_path_param_names(
1278            "/api/tenants/{tenant_id}/secrets/{secret_name}",
1279        );
1280        assert_eq!(
1281            names,
1282            Ok(vec![
1283                "tenant_id".to_string(),
1284                "secret_name".to_string(),
1285            ])
1286        );
1287    }
1288
1289    #[test]
1290    fn duplicate_placeholder_returns_err() {
1291        assert_eq!(
1292            extract_path_param_names("/api/users/{id}/posts/{id}"),
1293            Err("id".to_string())
1294        );
1295    }
1296
1297    #[test]
1298    fn underscore_and_numeric_in_name() {
1299        assert_eq!(
1300            extract_path_param_names("/api/{user_id}/items/{item_2}"),
1301            Ok(vec!["user_id".to_string(), "item_2".to_string()])
1302        );
1303    }
1304
1305    #[test]
1306    fn leading_underscore_accepted() {
1307        // Identifiers in HTTP paths often start with letters but the
1308        // grammar permits leading underscore (parity with Rust identifier
1309        // rules). The flow parameter name on the binding side has to
1310        // match exactly, so adopters with `_internal_id` in the path
1311        // can pair it with a same-named flow param.
1312        assert_eq!(
1313            extract_path_param_names("/api/{_internal}"),
1314            Ok(vec!["_internal".to_string()])
1315        );
1316    }
1317
1318    #[test]
1319    fn malformed_placeholder_silently_ignored() {
1320        // Content inside `{...}` that does not match the identifier
1321        // grammar is skipped at this layer. axum surfaces the route
1322        // registration failure if the literal text is invalid.
1323        assert_eq!(
1324            extract_path_param_names("/api/{not valid}"),
1325            Ok(vec![])
1326        );
1327        // Empty braces — same: skip silently.
1328        assert_eq!(extract_path_param_names("/api/{}"), Ok(vec![]));
1329        // Mixed: malformed brace skipped, valid placeholder kept.
1330        assert_eq!(
1331            extract_path_param_names("/api/{tenant id}/users/{id}"),
1332            Ok(vec!["id".to_string()])
1333        );
1334    }
1335
1336    #[test]
1337    fn unterminated_brace_returns_clean() {
1338        // Open brace with no close brace — give up without panicking.
1339        // (axum surfaces the malformed-route error at registration.)
1340        assert_eq!(extract_path_param_names("/api/{id"), Ok(vec![]));
1341    }
1342
1343    #[test]
1344    fn placeholders_at_path_boundaries() {
1345        // Placeholder as the very first segment AND the very last
1346        // segment — both should be extracted.
1347        assert_eq!(
1348            extract_path_param_names("{prefix}/api/users/{id}"),
1349            Ok(vec!["prefix".to_string(), "id".to_string()])
1350        );
1351        assert_eq!(
1352            extract_path_param_names("/api/{id}"),
1353            Ok(vec!["id".to_string()])
1354        );
1355    }
1356
1357    #[test]
1358    fn deduplication_detects_non_adjacent_duplicates() {
1359        // The duplicate-detection sweep is global, not just adjacent.
1360        assert_eq!(
1361            extract_path_param_names(
1362                "/api/orgs/{org_id}/teams/{team_id}/repos/{org_id}"
1363            ),
1364            Err("org_id".to_string())
1365        );
1366    }
1367
1368    #[test]
1369    fn never_panics_on_arbitrary_input() {
1370        // Light fuzz: a handful of weird inputs return cleanly.
1371        for input in &[
1372            "{",
1373            "}",
1374            "{}",
1375            "{{}}",
1376            "{{{",
1377            "/api/{}/{id}",
1378            "////",
1379            "\u{1F4A1}",        // emoji (lightbulb)
1380            "\u{0000}",         // null byte
1381        ] {
1382            let _ = extract_path_param_names(input); // must not panic
1383        }
1384    }
1385}
1386
1387#[cfg(test)]
1388mod capability_slug_tests {
1389    use super::is_valid_capability_slug;
1390
1391    #[test]
1392    fn accepts_canonical_examples() {
1393        assert!(is_valid_capability_slug("admin"));
1394        assert!(is_valid_capability_slug("legal.read"));
1395        assert!(is_valid_capability_slug("hipaa.phi.read"));
1396        assert!(is_valid_capability_slug("bank.officer.senior"));
1397        assert!(is_valid_capability_slug("a"));
1398        assert!(is_valid_capability_slug("a_b"));
1399        assert!(is_valid_capability_slug("a1"));
1400        assert!(is_valid_capability_slug("a.b1_c"));
1401    }
1402
1403    #[test]
1404    fn rejects_empty_string() {
1405        assert!(!is_valid_capability_slug(""));
1406    }
1407
1408    #[test]
1409    fn rejects_uppercase() {
1410        assert!(!is_valid_capability_slug("Admin"));
1411        assert!(!is_valid_capability_slug("admin.READ"));
1412    }
1413
1414    #[test]
1415    fn rejects_digit_first() {
1416        assert!(!is_valid_capability_slug("1admin"));
1417        assert!(!is_valid_capability_slug("admin.1read"));
1418    }
1419
1420    #[test]
1421    fn rejects_hyphen() {
1422        assert!(!is_valid_capability_slug("bank-officer"));
1423    }
1424
1425    #[test]
1426    fn rejects_empty_segments() {
1427        assert!(!is_valid_capability_slug("bank..a"));
1428        assert!(!is_valid_capability_slug(".admin"));
1429        assert!(!is_valid_capability_slug("admin."));
1430    }
1431
1432    #[test]
1433    fn rejects_special_chars() {
1434        assert!(!is_valid_capability_slug("admin@read"));
1435        assert!(!is_valid_capability_slug("admin/read"));
1436        assert!(!is_valid_capability_slug("admin read"));
1437    }
1438}
1439
1440// ── Parser ───────────────────────────────────────────────────────────────────
1441
1442pub struct Parser {
1443    tokens: Vec<Token>,
1444    pos: usize,
1445    /// §Fase 119.f — declarations lifted out of a FLOW BODY to program level.
1446    ///
1447    /// README nests an epistemic block inside a flow to scope the helper
1448    /// flows it calls:
1449    ///
1450    /// ```text
1451    /// flow MarketIntelligence(sector: String) -> Report {
1452    ///     know { flow GatherData(sector: String) -> DataSet { … } }
1453    ///     par { … }
1454    /// }
1455    /// ```
1456    ///
1457    /// A top-level `know { … }` already HOISTS its children into the
1458    /// program-level IR collections, stamping `epistemic_mode` on each
1459    /// (`ir_generator`, §99.d/§105/§110). Hoisting the nested one to a
1460    /// top-level `Declaration::Epistemic` therefore makes it byte-identical
1461    /// to the form that already works — zero new handling in the checker, the
1462    /// IR generator, or the runtime. The alternative (a new FlowStep variant
1463    /// carrying declarations) would fork every one of those.
1464    hoisted: Vec<Declaration>,
1465    /// Fase 14.a — leading trivia parallel array, indexed by the
1466    /// effective-token position. `leading_trivia[i]` is the comment
1467    /// trivia that appeared between the previous effective token (or
1468    /// file start) and `tokens[i]`.
1469    leading_trivia: Vec<Vec<Trivia>>,
1470    /// Fase 14.a — trailing trivia parallel array. `trailing_trivia[i]`
1471    /// is the comment trivia on the same line as `tokens[i]`, before
1472    /// the next effective token. Populated by the constructor.
1473    trailing_trivia: Vec<Vec<Trivia>>,
1474    /// Fase 17.a — side-channel for tagging let value_kind. Set by
1475    /// `parse_let_atom` / `parse_let_value_expr` as they descend; read
1476    /// at the end of `parse_let` and stored on the LetStatement.
1477    last_let_value_kind: String,
1478    /// Fase 19.e — loop nesting depth for break/continue scope check.
1479    /// Incremented at the start of `parse_for_in`, decremented after.
1480    /// `parse_break`/`parse_continue` raise ParseError when this is
1481    /// zero (the keyword has no meaning outside a loop body).
1482    loop_depth: u32,
1483    /// §Fase 28.d — Optional source text + filename for the rustc-
1484    /// style source-context block on `ParseError`. Set via the
1485    /// fluent `Parser::with_source` builder; default `None` keeps
1486    /// existing callers (`Parser::new(tokens).parse()`) emitting
1487    /// the legacy single-line shape.
1488    source: Option<String>,
1489    filename: String,
1490}
1491
1492impl Parser {
1493    pub fn new(raw_tokens: Vec<Token>) -> Self {
1494        // ── Fase 14.a — split the raw token stream into:
1495        //   - effective tokens the grammar consumes (cursor advances
1496        //     over these as before),
1497        //   - parallel `leading_trivia` / `trailing_trivia` arrays
1498        //     indexed by effective-token position.
1499        // Comments on a fresh line attach as leading trivia of the
1500        // next effective token; comments on the same line as an
1501        // effective token attach as trailing trivia of that token.
1502        // Roslyn/Swift convention.
1503        let mut effective: Vec<Token> = Vec::with_capacity(raw_tokens.len());
1504        let mut leading: Vec<Vec<Trivia>> = Vec::with_capacity(raw_tokens.len());
1505        let mut trailing: Vec<Vec<Trivia>> = Vec::with_capacity(raw_tokens.len());
1506
1507        let mut pending_leading: Vec<Trivia> = Vec::new();
1508        let mut last_effective_line: i64 = -1;
1509        for tok in raw_tokens {
1510            if is_comment_token(&tok.ttype) {
1511                let kind = token_to_trivia_kind(&tok.ttype)
1512                    .expect("comment token must map to a trivia kind");
1513                let triv = Trivia {
1514                    kind,
1515                    text: tok.value,
1516                    line: tok.line,
1517                    column: tok.column,
1518                };
1519                if !effective.is_empty() && (tok.line as i64) == last_effective_line {
1520                    trailing.last_mut().unwrap().push(triv);
1521                } else {
1522                    pending_leading.push(triv);
1523                }
1524            } else {
1525                last_effective_line = tok.line as i64;
1526                effective.push(tok);
1527                leading.push(std::mem::take(&mut pending_leading));
1528                trailing.push(Vec::new());
1529            }
1530        }
1531
1532        Parser {
1533            hoisted: Vec::new(),
1534            tokens: effective,
1535            pos: 0,
1536            leading_trivia: leading,
1537            trailing_trivia: trailing,
1538            last_let_value_kind: "literal".to_string(),
1539            loop_depth: 0,
1540            source: None,
1541            filename: "<source>".to_string(),
1542        }
1543    }
1544
1545    /// §Fase 28.d — Fluent attach of source text + filename for
1546    /// rustc-style source-context blocks on emitted `ParseError`s.
1547    /// Returns `self` so it chains with `.parse_with_recovery()`:
1548    ///
1549    /// ```ignore
1550    /// let result = Parser::new(tokens)
1551    ///     .with_source(src, "foo.axon")
1552    ///     .parse_with_recovery();
1553    /// ```
1554    ///
1555    /// No-op of any other behaviour — pure metadata attach.
1556    #[must_use]
1557    pub fn with_source(mut self, source: &str, filename: &str) -> Self {
1558        self.source = Some(source.to_string());
1559        self.filename = filename.to_string();
1560        self
1561    }
1562
1563    // ── public API ───────────────────────────────────────────────
1564
1565    pub fn parse(&mut self) -> Result<Program, ParseError> {
1566        let mut program = Program {
1567            declarations: Vec::new(),
1568            declaration_trivia: Vec::new(),
1569            loc: Loc { line: 1, column: 1 },
1570        };
1571        while !self.check(TokenType::Eof) {
1572            // Capture trivia around the declaration. `start_pos` is
1573            // the effective-token position of the declaration's first
1574            // token; that position carries the leading trivia. After
1575            // parsing, `pos - 1` is the last token consumed; that
1576            // position carries the trailing trivia.
1577            let start_pos = self.pos;
1578            let mut decl = match self.parse_declaration() {
1579                Ok(d) => d,
1580                Err(e) => return Err(self.attach_source_to_error(e)),
1581            };
1582            let end_pos = self.pos.saturating_sub(1);
1583            let leading = self
1584                .leading_trivia
1585                .get(start_pos)
1586                .cloned()
1587                .unwrap_or_default();
1588            let trailing = self
1589                .trailing_trivia
1590                .get(end_pos)
1591                .cloned()
1592                .unwrap_or_default();
1593            // Fase 14.b — also copy trivia into the per-struct fields on
1594            // the declaration so consumers can read `flow.leading_trivia`
1595            // directly without going through `program.declaration_trivia[i]`.
1596            // The side-channel is preserved for backward compat with
1597            // 14.a callers and as a flat enumeration source.
1598            attach_trivia_to_decl(&mut decl, leading.clone(), trailing.clone());
1599            program.declarations.push(decl);
1600            program
1601                .declaration_trivia
1602                .push(DeclarationTrivia { leading, trailing });
1603            // §Fase 119.f — drain anything a flow body hoisted to program
1604            // level. Appended AFTER the enclosing declaration so source order
1605            // still reads top-to-bottom in `axon desugar`.
1606            for hoisted in std::mem::take(&mut self.hoisted) {
1607                program.declarations.push(hoisted);
1608                program.declaration_trivia.push(DeclarationTrivia {
1609                    leading: Vec::new(),
1610                    trailing: Vec::new(),
1611                });
1612            }
1613        }
1614        // §Fase 80.g — expand `voice` declarations FIRST (they may emit
1615        // `from Preset@vN` upstream legs), then §80.f preset references,
1616        // BEFORE type-check — so the §80.c laws and the IR see the expanded
1617        // program (and `axon desugar` prints exactly this lowering).
1618        // Unknown presets stay unexpanded — the checker reports them with
1619        // the catalog list (accumulating diagnostics beat a parse abort).
1620        crate::voice_desugar::expand(&mut program);
1621        crate::upstream_presets::expand(&mut program);
1622        Ok(program)
1623    }
1624
1625    // ── §Fase 28.c — recovery-mode parse ─────────────────────────
1626    //
1627    // Mirror of Python's `Parser.parse_with_recovery` from
1628    // `axon/compiler/parser.py`. Wraps `parse_declaration` in a
1629    // try/recover loop: on any `ParseError` the error is appended to
1630    // the list and the cursor advances to the next sync point, then
1631    // parsing resumes. The two stacks must produce structurally
1632    // identical error lists on the same input — that is the cross-
1633    // stack drift gate (D7). See the test module
1634    // `tests::fase28_recovery_tests` and Python-side
1635    // `tests/test_fase28_parser_recovery.py`.
1636
1637    /// Recovery-mode parse. Collects every parse error in source
1638    /// order; the existing `parse()` API remains fail-fast (D9).
1639    ///
1640    /// # Recovery contract (D2)
1641    ///
1642    /// On `ParseError`:
1643    ///   1. Push the error onto `errors`.
1644    ///   2. If the cursor is already on a top-level declaration
1645    ///      keyword (and brace-depth ≤ 0), do not consume — the
1646    ///      caller should retry the declaration parse from here.
1647    ///      Otherwise advance one token to make progress, then
1648    ///      walk to the next sync point.
1649    ///   3. Resume the outer loop.
1650    ///
1651    /// Sync points: top-level declaration keyword at brace-depth ≤ 0,
1652    /// or EOF. Negative depths are treated identically to ≤ 0 — the
1653    /// walker keeps walking through over-balanced `}` rather than
1654    /// pretending a closing brace is itself a sync point (which would
1655    /// emit a ghost "Unexpected token at top level" error in the
1656    /// outer loop).
1657    pub fn parse_with_recovery(&mut self) -> ParseResult {
1658        let mut program = Program {
1659            declarations: Vec::new(),
1660            declaration_trivia: Vec::new(),
1661            loc: Loc { line: 1, column: 1 },
1662        };
1663        let mut errors: Vec<ParseError> = Vec::new();
1664
1665        while !self.check(TokenType::Eof) {
1666            let start_pos = self.pos;
1667            match self.parse_declaration() {
1668                Ok(mut decl) => {
1669                    let end_pos = self.pos.saturating_sub(1);
1670                    let leading = self
1671                        .leading_trivia
1672                        .get(start_pos)
1673                        .cloned()
1674                        .unwrap_or_default();
1675                    let trailing = self
1676                        .trailing_trivia
1677                        .get(end_pos)
1678                        .cloned()
1679                        .unwrap_or_default();
1680                    attach_trivia_to_decl(&mut decl, leading.clone(), trailing.clone());
1681                    program.declarations.push(decl);
1682                    program
1683                        .declaration_trivia
1684                        .push(DeclarationTrivia { leading, trailing });
1685                }
1686                Err(err) => {
1687                    // §Fase 28.d — attach source-context block when a
1688                    // source has been provided via `with_source(...)`;
1689                    // otherwise the error keeps its single-line shape.
1690                    errors.push(self.attach_source_to_error(err));
1691                    // Make progress. If parse_declaration returned
1692                    // immediately on the same token (e.g. unknown
1693                    // top-level token), we MUST advance at least one
1694                    // token to avoid an infinite loop.
1695                    if self.pos == start_pos && !self.check(TokenType::Eof) {
1696                        self.advance();
1697                    }
1698                    self.advance_to_sync_point();
1699                }
1700            }
1701        }
1702
1703        ParseResult { program, errors }
1704    }
1705
1706    /// §Fase 28.d — Decorate a `ParseError` with a `SourceSnippet`
1707    /// when the parser has source context attached, otherwise return
1708    /// the error unchanged. Idempotent: if the error already carries
1709    /// a snippet, this overwrites it with the parser's source.
1710    fn attach_source_to_error(&self, err: ParseError) -> ParseError {
1711        match &self.source {
1712            Some(src) if err.line >= 1 => err.attach_source(src, &self.filename),
1713            _ => err,
1714        }
1715    }
1716
1717    /// §Fase 28.c — Walk the cursor forward until the next sync
1718    /// point (top-level declaration keyword at brace-depth ≤ 0) or
1719    /// EOF. Used by `parse_with_recovery` to skip the malformed
1720    /// remainder of a failed declaration.
1721    fn advance_to_sync_point(&mut self) {
1722        let mut depth: i32 = 0;
1723        while !self.check(TokenType::Eof) {
1724            let tt = self.current().ttype.clone();
1725            // Sync at top-level keywords when depth ≤ 0. We do not
1726            // consume the keyword — the outer loop will dispatch on
1727            // it.
1728            if is_top_level_decl_kw_for_recovery(&tt) && depth <= 0 {
1729                return;
1730            }
1731            if matches!(tt, TokenType::LBrace) {
1732                depth += 1;
1733            } else if matches!(tt, TokenType::RBrace) {
1734                depth -= 1;
1735            }
1736            self.advance();
1737        }
1738    }
1739
1740    // ── token helpers ────────────────────────────────────────────
1741
1742    fn current(&self) -> &Token {
1743        if self.pos >= self.tokens.len() {
1744            self.tokens.last().unwrap() // EOF sentinel
1745        } else {
1746            &self.tokens[self.pos]
1747        }
1748    }
1749
1750    fn advance(&mut self) -> &Token {
1751        let idx = self.pos;
1752        if self.pos < self.tokens.len() {
1753            self.pos += 1;
1754        }
1755        &self.tokens[idx]
1756    }
1757
1758    fn check(&self, tt: TokenType) -> bool {
1759        self.current().ttype == tt
1760    }
1761
1762    fn consume(&mut self, expected: TokenType) -> Result<Token, ParseError> {
1763        let tok = self.current().clone();
1764        if tok.ttype != expected {
1765            return Err(ParseError {
1766                message: format!(
1767                    "Expected {:?}, found {:?}('{}')",
1768                    expected, tok.ttype, tok.value
1769                ),
1770                line: tok.line,
1771                column: tok.column,
1772                            ..Default::default()
1773            });
1774        }
1775        self.pos += 1;
1776        Ok(tok)
1777    }
1778
1779    /// §Fase 41.b — build a `ParseError` at the current token's location.
1780    fn error(&self, message: &str) -> ParseError {
1781        let tok = self.current();
1782        ParseError { message: message.to_string(), line: tok.line, column: tok.column, ..Default::default() }
1783    }
1784
1785    /// Consume any identifier or keyword-used-as-value.
1786    fn consume_any_ident_or_kw(&mut self) -> Result<Token, ParseError> {
1787        let tok = self.current().clone();
1788        match tok.ttype {
1789            TokenType::Identifier
1790            | TokenType::Bool
1791            | TokenType::StringLit
1792            | TokenType::Integer
1793            | TokenType::Float => {
1794                self.pos += 1;
1795                Ok(tok)
1796            }
1797            _ => {
1798                // Allow any keyword token whose value is alphabetic
1799                if !tok.value.is_empty()
1800                    && tok.value.chars().all(|c| c.is_alphanumeric() || c == '_')
1801                    && tok.ttype != TokenType::Eof
1802                {
1803                    self.pos += 1;
1804                    Ok(tok)
1805                } else {
1806                    Err(ParseError {
1807                        message: format!(
1808                            "Expected identifier or keyword value, found {:?}('{}')",
1809                            tok.ttype, tok.value
1810                        ),
1811                        line: tok.line,
1812                        column: tok.column,
1813                                            ..Default::default()
1814                    })
1815                }
1816            }
1817        }
1818    }
1819
1820    fn consume_number(&mut self) -> Result<f64, ParseError> {
1821        let tok = self.current().clone();
1822        match tok.ttype {
1823            TokenType::Float | TokenType::Integer => {
1824                self.pos += 1;
1825                tok.value.parse::<f64>().map_err(|_| ParseError {
1826                    message: format!("Invalid number '{}'", tok.value),
1827                    line: tok.line,
1828                    column: tok.column,
1829                                    ..Default::default()
1830                })
1831            }
1832            _ => Err(ParseError {
1833                message: format!("Expected number, found {:?}('{}')", tok.ttype, tok.value),
1834                line: tok.line,
1835                column: tok.column,
1836                            ..Default::default()
1837            }),
1838        }
1839    }
1840
1841    fn parse_bool(&mut self) -> Result<bool, ParseError> {
1842        let tok = self.consume(TokenType::Bool)?;
1843        Ok(tok.value == "true")
1844    }
1845
1846    fn loc_of(&self, tok: &Token) -> Loc {
1847        Loc {
1848            line: tok.line,
1849            column: tok.column,
1850        }
1851    }
1852
1853    fn check_run_modifier(&self) -> bool {
1854        // §Fase 119.m.3 — `with <Persona>` is the spelling README uses on every
1855        // `run` it publishes; `as <Persona>` is the one the parser took. Same
1856        // position, same meaning, and the two cannot be confused: `with` is not
1857        // a keyword token, and the only OTHER `with` in the language sits after
1858        // a tool name inside a step body (`use_tool T with k: v`), which this
1859        // predicate is never consulted at.
1860        if self.current().value == "with" {
1861            return true;
1862        }
1863        matches!(
1864            self.current().ttype,
1865            TokenType::As
1866                | TokenType::Within
1867                | TokenType::ConstrainedBy
1868                | TokenType::OnFailure
1869                | TokenType::OutputTo
1870                | TokenType::Effort
1871        )
1872    }
1873
1874    // ── list helpers ─────────────────────────────────────────────
1875
1876    fn parse_string_list(&mut self) -> Result<Vec<String>, ParseError> {
1877        self.consume(TokenType::LBracket)?;
1878        let mut items = Vec::new();
1879        items.push(self.consume(TokenType::StringLit)?.value);
1880        while self.check(TokenType::Comma) {
1881            self.advance();
1882            items.push(self.consume(TokenType::StringLit)?.value);
1883        }
1884        self.consume(TokenType::RBracket)?;
1885        Ok(items)
1886    }
1887
1888    /// §Fase 83.a — a bracketed list of quoted string literals, tolerant of
1889    /// an empty `[]` and a trailing comma before `]` (the `Window.exclude`
1890    /// shape, generalized into a reusable helper). Used for CORS field
1891    /// lists whose values contain characters (`://`, `.`, `-`) that aren't
1892    /// valid bare identifiers — `allow_origins`, `allow_headers`,
1893    /// `expose_headers` — where `parse_string_list`'s "at least one item,
1894    /// no trailing comma" strictness would reject a legitimate empty or
1895    /// comma-terminated declaration.
1896    fn parse_bracketed_strings(&mut self) -> Result<Vec<String>, ParseError> {
1897        self.consume(TokenType::LBracket)?;
1898        let mut items = Vec::new();
1899        if !self.check(TokenType::RBracket) {
1900            items.push(self.consume(TokenType::StringLit)?.value);
1901            while self.check(TokenType::Comma) {
1902                self.advance();
1903                if self.check(TokenType::RBracket) {
1904                    break; // trailing comma
1905                }
1906                items.push(self.consume(TokenType::StringLit)?.value);
1907            }
1908        }
1909        self.consume(TokenType::RBracket)?;
1910        Ok(items)
1911    }
1912
1913    fn parse_identifier_list(&mut self) -> Result<Vec<String>, ParseError> {
1914        let mut names = Vec::new();
1915        names.push(self.consume(TokenType::Identifier)?.value);
1916        while self.check(TokenType::Comma) {
1917            self.advance();
1918            names.push(self.consume(TokenType::Identifier)?.value);
1919        }
1920        Ok(names)
1921    }
1922
1923    fn parse_bracketed_identifiers(&mut self) -> Result<Vec<String>, ParseError> {
1924        self.consume(TokenType::LBracket)?;
1925        let items = self.parse_extended_identifier_list()?;
1926        self.consume(TokenType::RBracket)?;
1927        Ok(items)
1928    }
1929
1930    fn parse_extended_identifier_list(&mut self) -> Result<Vec<String>, ParseError> {
1931        let mut items = Vec::new();
1932        items.push(self.consume_any_ident_or_kw()?.value);
1933        while self.check(TokenType::Comma) {
1934            self.advance();
1935            items.push(self.consume_any_ident_or_kw()?.value);
1936        }
1937        Ok(items)
1938    }
1939
1940    fn parse_dotted_identifier(&mut self) -> Result<String, ParseError> {
1941        let mut parts = vec![self.consume_any_ident_or_kw()?.value];
1942        while self.check(TokenType::Dot) {
1943            self.advance();
1944            parts.push(self.consume_any_ident_or_kw()?.value);
1945        }
1946        Ok(parts.join("."))
1947    }
1948
1949    /// §Fase 119.f.10 — a **SUBJECT**: the thing a statement acts ON.
1950    ///
1951    /// Every statement in the language has two kinds of operand, and they had
1952    /// been parsed by the same function:
1953    ///
1954    ///   - a **NAME** — the declaration being applied (`compute CalculatePremium`,
1955    ///     `mandate SECFormat`, `use_tool WebSearch`). Always a bare identifier;
1956    ///     a dotted name would refer to nothing.
1957    ///   - a **SUBJECT** — what it acts on (`validate Assess.output`,
1958    ///     `compute X on Analyze.risk_factor, 1.2`). A reference, and a
1959    ///     reference in Axon is DOTTED: `Assess.output` is the canonical way one
1960    ///     step names another's result, and it already parses inside `given:`,
1961    ///     inside `use_tool … with k: v`, and in `navigate_ref`.
1962    ///
1963    /// Subject positions called `consume_any_ident_or_kw`, which stops at the
1964    /// dot. So the reference form the whole language is built on was rejected in
1965    /// exactly the position that most needs it — five README blocks fail on it
1966    /// as their FIRST error and three more need it further in.
1967    ///
1968    /// Literals are subjects too (`compute X on Profile.tenure, 1.2, "USD"`).
1969    /// A string literal keeps its quotes here so the runtime can tell a literal
1970    /// from a binding name — the §60 classification, preserved instead of
1971    /// flattened.
1972    fn parse_subject(&mut self) -> Result<String, ParseError> {
1973        let t = self.current().clone();
1974        match t.ttype {
1975            TokenType::StringLit => {
1976                self.advance();
1977                Ok(format!("\"{}\"", t.value))
1978            }
1979            TokenType::Integer | TokenType::Float => {
1980                self.advance();
1981                Ok(t.value)
1982            }
1983            _ => self.parse_dotted_identifier(),
1984        }
1985    }
1986
1987    fn parse_expression_string(&mut self) -> Result<String, ParseError> {
1988        if self.check(TokenType::LBracket) {
1989            let items = self.parse_bracketed_dot_identifiers()?;
1990            return Ok(format!("[{}]", items.join(", ")));
1991        }
1992        self.parse_dotted_identifier()
1993    }
1994
1995    fn parse_bracketed_dot_identifiers(&mut self) -> Result<Vec<String>, ParseError> {
1996        self.consume(TokenType::LBracket)?;
1997        let mut items = vec![self.parse_dotted_identifier()?];
1998        while self.check(TokenType::Comma) {
1999            self.advance();
2000            items.push(self.parse_dotted_identifier()?);
2001        }
2002        self.consume(TokenType::RBracket)?;
2003        Ok(items)
2004    }
2005
2006    fn parse_argument_list(&mut self) -> Result<Vec<String>, ParseError> {
2007        let mut args = Vec::new();
2008        while !self.check(TokenType::RParen) {
2009            let tok = self.current().clone();
2010            match tok.ttype {
2011                TokenType::StringLit | TokenType::Integer | TokenType::Float => {
2012                    self.advance();
2013                    args.push(tok.value);
2014                }
2015                TokenType::Identifier => {
2016                    self.advance();
2017                    let mut val = tok.value;
2018                    if self.check(TokenType::Dot) {
2019                        self.advance();
2020                        val.push('.');
2021                        val.push_str(&self.consume_any_ident_or_kw()?.value);
2022                    }
2023                    args.push(val);
2024                }
2025                _ => {
2026                    self.advance();
2027                    let key = tok.value;
2028                    if self.check(TokenType::Colon) {
2029                        self.advance();
2030                        let v = self.advance().value.clone();
2031                        args.push(format!("{key}:{v}"));
2032                    } else {
2033                        args.push(key);
2034                    }
2035                }
2036            }
2037            if self.check(TokenType::Comma) {
2038                self.advance();
2039            }
2040        }
2041        Ok(args)
2042    }
2043
2044    /// Skip a single value or balanced bracketed/braced block (unknown field).
2045    fn skip_value(&mut self) {
2046        match self.current().ttype {
2047            TokenType::LBracket => {
2048                self.advance();
2049                let mut depth = 1u32;
2050                while depth > 0 && !self.check(TokenType::Eof) {
2051                    if self.check(TokenType::LBracket) {
2052                        depth += 1;
2053                    } else if self.check(TokenType::RBracket) {
2054                        depth -= 1;
2055                    }
2056                    self.advance();
2057                }
2058            }
2059            TokenType::LBrace => {
2060                self.advance();
2061                let mut depth = 1u32;
2062                while depth > 0 && !self.check(TokenType::Eof) {
2063                    if self.check(TokenType::LBrace) {
2064                        depth += 1;
2065                    } else if self.check(TokenType::RBrace) {
2066                        depth -= 1;
2067                    }
2068                    self.advance();
2069                }
2070            }
2071            TokenType::Lt => {
2072                // effect row: <io, network, ...>
2073                self.advance();
2074                let mut depth = 1u32;
2075                while depth > 0 && !self.check(TokenType::Eof) {
2076                    if self.check(TokenType::Lt) {
2077                        depth += 1;
2078                    } else if self.check(TokenType::Gt) {
2079                        depth -= 1;
2080                    }
2081                    self.advance();
2082                }
2083            }
2084            _ => {
2085                self.advance();
2086                while self.check(TokenType::Dot) {
2087                    self.advance();
2088                    self.advance();
2089                }
2090            }
2091        }
2092    }
2093
2094    /// Skip a balanced `{ ... }` block including its braces.
2095    fn skip_braced_block(&mut self) -> Result<(), ParseError> {
2096        self.consume(TokenType::LBrace)?;
2097        let mut depth = 1u32;
2098        while depth > 0 {
2099            if self.check(TokenType::Eof) {
2100                let tok = self.current();
2101                return Err(ParseError {
2102                    message: "Unterminated block — expected '}'".to_string(),
2103                    line: tok.line,
2104                    column: tok.column,
2105                                    ..Default::default()
2106                });
2107            }
2108            if self.check(TokenType::LBrace) {
2109                depth += 1;
2110            } else if self.check(TokenType::RBrace) {
2111                depth -= 1;
2112            }
2113            self.advance();
2114        }
2115        Ok(())
2116    }
2117
2118    fn at_declaration_start(&self) -> bool {
2119        is_declaration_keyword(&self.current().ttype) || self.check(TokenType::Eof)
2120    }
2121
2122    // ── top-level dispatch ───────────────────────────────────────
2123
2124    fn parse_declaration(&mut self) -> Result<Declaration, ParseError> {
2125        let tok = self.current().clone();
2126
2127        // §Fase 114.a — a TOP-LEVEL `budget <Name> { … }`.
2128        //
2129        // `budget` lexes as `TokenType::Budget` (the daemon-field keyword). At top
2130        // level it is only a declaration when a NAME follows — `budget Foo { … }`.
2131        // The lookahead is what keeps the daemon-attached form (`daemon D { budget
2132        // { … } }`, where `{` follows immediately) untouched: there the next token
2133        // is `{`, not an identifier, so this branch does not fire.
2134        if tok.ttype == TokenType::Budget && self.peek_is_identifier() {
2135            return self.parse_top_level_budget().map(Declaration::Budget);
2136        }
2137
2138        match tok.ttype {
2139            TokenType::Import => self.parse_import().map(Declaration::Import),
2140            TokenType::Persona => self.parse_persona().map(Declaration::Persona),
2141            TokenType::Context => self.parse_context().map(Declaration::Context),
2142            TokenType::Anchor => self.parse_anchor().map(Declaration::Anchor),
2143            TokenType::Memory => self.parse_memory().map(Declaration::Memory),
2144            TokenType::Tool => self.parse_tool().map(Declaration::Tool),
2145            TokenType::Type => self.parse_type_def().map(Declaration::Type),
2146            TokenType::Flow => self.parse_flow().map(Declaration::Flow),
2147            // §Fase 120 — `effect E { Op(p: T) -> R }`. A peer of `tool`, per
2148            // `fase_23` §3.1 ("top-level, like tool/persona/anchor").
2149            TokenType::Effect => self.parse_effect().map(Declaration::Effect),
2150            TokenType::Intent => self.parse_intent().map(Declaration::Intent),
2151            TokenType::Run => self.parse_run().map(Declaration::Run),
2152            TokenType::Let => self.parse_let().map(Declaration::Let),
2153            TokenType::Know | TokenType::Believe | TokenType::Speculate | TokenType::Doubt => {
2154                self.parse_epistemic_block().map(Declaration::Epistemic)
2155            }
2156            TokenType::Lambda => self.parse_lambda_data().map(Declaration::LambdaData),
2157
2158            // ── Tier 2 declarations (full AST) ──────────────────
2159            TokenType::Agent => self.parse_agent().map(Declaration::Agent),
2160            TokenType::Shield => self.parse_shield().map(Declaration::Shield),
2161            // §Fase 71.a — temporal execution-window guard.
2162            TokenType::Window => self.parse_window().map(Declaration::Window),
2163            TokenType::Pix => self.parse_pix().map(Declaration::Pix),
2164            TokenType::Ledger => self.parse_ledger().map(Declaration::Ledger),
2165            TokenType::Psyche => self.parse_psyche().map(Declaration::Psyche),
2166            TokenType::Corpus => self.parse_corpus().map(Declaration::Corpus),
2167            TokenType::Dataspace => self.parse_dataspace().map(Declaration::Dataspace),
2168            TokenType::Ots => self.parse_ots().map(Declaration::Ots),
2169            TokenType::Mandate => self.parse_mandate().map(Declaration::Mandate),
2170            TokenType::Compute => self.parse_compute().map(Declaration::Compute),
2171            TokenType::Daemon => self.parse_daemon().map(Declaration::Daemon),
2172            TokenType::Extension => self.parse_extension().map(Declaration::Extension),
2173            TokenType::AxonStore => self.parse_axonstore().map(Declaration::AxonStore),
2174            TokenType::AxonEndpoint => self.parse_axonendpoint().map(Declaration::AxonEndpoint),
2175
2176            // ── §λ-L-E Fase 1 — I/O cognitivo ───────────────────
2177            TokenType::Resource => self.parse_resource().map(Declaration::Resource),
2178            TokenType::Fabric => self.parse_fabric().map(Declaration::Fabric),
2179            TokenType::Manifest => self.parse_manifest().map(Declaration::Manifest),
2180            TokenType::Observe => self.parse_observe().map(Declaration::Observe),
2181
2182            // ── §λ-L-E Fase 3 — Control cognitivo ───────────────
2183            TokenType::Reconcile => self.parse_reconcile().map(Declaration::Reconcile),
2184            TokenType::Lease => self.parse_lease().map(Declaration::Lease),
2185            TokenType::Ensemble => self.parse_ensemble().map(Declaration::Ensemble),
2186
2187            // ── §λ-L-E Fase 4 — Topology + π-calculus sessions ─
2188            TokenType::Session => self.parse_session_definition().map(Declaration::Session),
2189            TokenType::Topology => self.parse_topology().map(Declaration::Topology),
2190
2191            // ── §Fase 41.b — typed WebSocket transport ─────────
2192            TokenType::Socket => self.parse_socket().map(Declaration::Socket),
2193
2194            // ── §Fase 80.b — outbound vendor connection ─────────
2195            TokenType::Upstream => self.parse_upstream().map(Declaration::Upstream),
2196
2197            // ── §Fase 80.g — the voice-agent simplicity layer ───
2198            TokenType::Voice => self.parse_voice().map(Declaration::Voice),
2199
2200            // ── §Fase 83.a — the named origin-policy declaration ─
2201            TokenType::Cors => self.parse_cors().map(Declaration::Cors),
2202
2203            // ── §Fase 85.a — the named result-memoization policy ─
2204            TokenType::Cache => self.parse_cache().map(Declaration::Cache),
2205            TokenType::Document => self.parse_document().map(Declaration::Document),
2206
2207            // ── §Fase 105 — Governed CRM Delivery ─
2208            TokenType::Deliver => self.parse_deliver().map(Declaration::Deliver),
2209            TokenType::Notify => self.parse_notify().map(Declaration::Notify),
2210
2211            // ── §Fase 87.a — the long-horizon autonomous research primitive ─
2212            TokenType::Savant => self.parse_savant().map(Declaration::Savant),
2213
2214            // ── §Fase 87.d — the dynamic tool-synthesis policy ──────────────
2215            TokenType::Synth => self.parse_synth().map(Declaration::Synth),
2216
2217            // ── §Fase 88.a — the authorization-scope policy declaration ─────
2218            TokenType::Scope => self.parse_scope().map(Declaration::Scope),
2219
2220            // ── §Fase 92.a — the ephemeral-credential contract ──────────────
2221            TokenType::Credential => self.parse_credential().map(Declaration::Credential),
2222
2223            // ── §Fase 51.c.2 — Pauli-sum observable ────────────
2224            TokenType::Observable => self.parse_observable().map(Declaration::Observable),
2225
2226            // ── §Fase 69.a — Advantage Witness ──────────────────
2227            TokenType::Witness => self.parse_witness().map(Declaration::Witness),
2228
2229            // ── §λ-L-E Fase 5 — Cognitive immune system ─────────
2230            TokenType::Immune => self.parse_immune().map(Declaration::Immune),
2231            TokenType::Reflex => self.parse_reflex().map(Declaration::Reflex),
2232            TokenType::Heal => self.parse_heal().map(Declaration::Heal),
2233
2234            // ── §λ-L-E Fase 9 — UI cognitiva ────────────────────
2235            TokenType::Component => self.parse_component().map(Declaration::Component),
2236            TokenType::View => self.parse_view().map(Declaration::View),
2237
2238            // ── §λ-L-E Fase 13 — Mobile typed channels ──────────
2239            TokenType::Channel => self.parse_channel().map(Declaration::Channel),
2240
2241            // ── Tier 3+ structural fallback ─────────────────────
2242            // Store operations: keyword target { ... } or keyword target ...
2243            TokenType::Ingest
2244            | TokenType::Persist
2245            | TokenType::Retrieve
2246            | TokenType::Mutate
2247            | TokenType::Purge
2248            | TokenType::Transact => self.parse_generic_declaration(),
2249
2250            // MCP declaration
2251            TokenType::Mcp => self.parse_generic_declaration(),
2252
2253            _ => {
2254                // §Fase 28.e — append "Did you mean X?" hint when the
2255                // unknown token looks like a typo'd top-level keyword
2256                // (Levenshtein ≤ 2). D3, D11 ratified 2026-05-10.
2257                let hint = crate::smart_suggest::suggest_for(
2258                    &tok.value,
2259                    crate::smart_suggest::TOP_LEVEL_KEYWORD_NAMES,
2260                );
2261                let base = format!(
2262                    "Unexpected token at top level: '{}' — expected declaration \
2263                     (persona, context, anchor, flow, run, ...)",
2264                    tok.value
2265                );
2266                let message = if hint.is_empty() {
2267                    base
2268                } else {
2269                    format!("{base}. {hint}")
2270                };
2271                Err(ParseError {
2272                    message,
2273                    line: tok.line,
2274                    column: tok.column,
2275                    ..Default::default()
2276                })
2277            }
2278        }
2279    }
2280
2281    // ── IMPORT ───────────────────────────────────────────────────
2282
2283    fn parse_import(&mut self) -> Result<ImportNode, ParseError> {
2284        let tok = self.consume(TokenType::Import)?;
2285        let loc = self.loc_of(&tok);
2286
2287        let mut path_parts = Vec::new();
2288
2289        // Optional @ scope
2290        if self.check(TokenType::At) {
2291            self.advance();
2292            let first = self.consume(TokenType::Identifier)?;
2293            path_parts.push(format!("@{}", first.value));
2294        } else {
2295            let first = self.consume(TokenType::Identifier)?;
2296            path_parts.push(first.value);
2297        }
2298
2299        while self.check(TokenType::Dot) {
2300            self.advance();
2301            if self.check(TokenType::LBrace) {
2302                break;
2303            }
2304            let part = self.consume(TokenType::Identifier)?;
2305            path_parts.push(part.value);
2306        }
2307
2308        let mut names = Vec::new();
2309        if self.check(TokenType::LBrace) {
2310            self.advance();
2311            names = self.parse_identifier_list()?;
2312            self.consume(TokenType::RBrace)?;
2313        }
2314
2315        // ── §Fase 115.c — the `@allow_downgrade` ECC valve ───────────────
2316        //
2317        // `import a.b.{X} @allow_downgrade` acknowledges an epistemic
2318        // downgrade across this edge (see `epistemic_compat.rs`). The
2319        // annotation position is unambiguous: no top-level declaration
2320        // begins with `@`, so an `@` here belongs to this import — and an
2321        // unknown annotation is refused with the fix in the message
2322        // rather than surfacing later as an opaque parse error.
2323        let mut allow_downgrade = false;
2324        if self.check(TokenType::At) {
2325            let at_tok = self.current().clone();
2326            self.advance();
2327            let ident = self.consume(TokenType::Identifier)?;
2328            if ident.value == "allow_downgrade" {
2329                allow_downgrade = true;
2330            } else {
2331                return Err(ParseError {
2332                    message: format!(
2333                        "unknown import annotation '@{}' — the only import annotation is \
2334                         `@allow_downgrade` (the §115 epistemic-downgrade acknowledgment).",
2335                        ident.value
2336                    ),
2337                    line: at_tok.line,
2338                    column: at_tok.column,
2339                    ..Default::default()
2340                });
2341            }
2342        }
2343
2344        // ── §Fase 111 — `apx` is RETRACTED ───────────────────────────────
2345        //
2346        // `import X with apx { … }` used to parse and then call
2347        // `skip_braced_block()` — the policy was consumed and thrown on the
2348        // floor. It never reached the AST, let alone the IR. In `axon-rs` the
2349        // string "apx" occurred only inside comments: there is no APX crate,
2350        // no binary, no MEC/PCC dependency verification, no EPR ranking, no
2351        // quarantine and no compliance gate. The public README advertised all
2352        // five.
2353        //
2354        // A dependency policy that silently evaporates is the worst possible
2355        // shape for this particular promise: the adopter believes their supply
2356        // chain is being verified, which is exactly the belief that stops them
2357        // from verifying it themselves. Refuse, loudly.
2358        let next_is_apx = self
2359            .tokens
2360            .get(self.pos + 1)
2361            .map(|t| t.value == "apx")
2362            .unwrap_or(false);
2363        if self.current().value == "with" && next_is_apx {
2364            let tok = self.current().clone();
2365            return Err(ParseError {
2366                message: "`import … with apx { … }` is RETRACTED (§111). The apx policy block was \
2367                          parsed and silently DISCARDED — it never reached the IR, and no epistemic \
2368                          dependency manager exists: no MEC/PCC verification, no EPR ranking, no \
2369                          quarantine, no compliance gate. Declaring it verified nothing while \
2370                          implying your supply chain was checked. Remove the `with apx { … }` \
2371                          clause; the plain `import` resolves through the §115 Epistemic Module \
2372                          System."
2373                    .to_string(),
2374                line: tok.line,
2375                column: tok.column,
2376                ..Default::default()
2377            });
2378        }
2379
2380        Ok(ImportNode {
2381            module_path: path_parts,
2382            names,
2383            allow_downgrade,
2384            loc,
2385            leading_trivia: Vec::new(),
2386            trailing_trivia: Vec::new(),
2387        })
2388    }
2389
2390    // ── PERSONA ──────────────────────────────────────────────────
2391
2392    fn parse_persona(&mut self) -> Result<PersonaDefinition, ParseError> {
2393        let tok = self.consume(TokenType::Persona)?;
2394        let loc = self.loc_of(&tok);
2395        let name = self.consume(TokenType::Identifier)?.value;
2396        self.consume(TokenType::LBrace)?;
2397
2398        let mut node = PersonaDefinition {
2399            name,
2400            domain: Vec::new(),
2401            tone: String::new(),
2402            confidence_threshold: None,
2403            cite_sources: None,
2404            refuse_if: Vec::new(),
2405            language: String::new(),
2406            description: String::new(),
2407            loc,
2408            leading_trivia: Vec::new(),
2409            trailing_trivia: Vec::new(),
2410        };
2411
2412        while !self.check(TokenType::RBrace) {
2413            let field_name = self.current().value.clone();
2414            self.advance();
2415            self.consume(TokenType::Colon)?;
2416
2417            match field_name.as_str() {
2418                "domain" => node.domain = self.parse_string_list()?,
2419                "tone" => node.tone = self.consume_any_ident_or_kw()?.value,
2420                "confidence_threshold" => node.confidence_threshold = Some(self.consume_number()?),
2421                "cite_sources" => node.cite_sources = Some(self.parse_bool()?),
2422                "refuse_if" => node.refuse_if = self.parse_bracketed_identifiers()?,
2423                "language" => node.language = self.consume(TokenType::StringLit)?.value,
2424                "description" => node.description = self.consume(TokenType::StringLit)?.value,
2425                _ => self.skip_value(),
2426            }
2427        }
2428        self.consume(TokenType::RBrace)?;
2429        Ok(node)
2430    }
2431
2432    // ── CONTEXT ──────────────────────────────────────────────────
2433
2434    fn parse_context(&mut self) -> Result<ContextDefinition, ParseError> {
2435        let tok = self.consume(TokenType::Context)?;
2436        let loc = self.loc_of(&tok);
2437        let name = self.consume(TokenType::Identifier)?.value;
2438        self.consume(TokenType::LBrace)?;
2439
2440        let mut node = ContextDefinition {
2441            name,
2442            memory_scope: String::new(),
2443            language: String::new(),
2444            depth: String::new(),
2445            max_tokens: None,
2446            temperature: None,
2447            cite_sources: None,
2448            now_tz: None,
2449            loc,
2450            leading_trivia: Vec::new(),
2451            trailing_trivia: Vec::new(),
2452        };
2453
2454        while !self.check(TokenType::RBrace) {
2455            let field_name = self.current().value.clone();
2456            self.advance();
2457            self.consume(TokenType::Colon)?;
2458
2459            match field_name.as_str() {
2460                "memory" => node.memory_scope = self.consume_any_ident_or_kw()?.value,
2461                "language" => node.language = self.consume(TokenType::StringLit)?.value,
2462                "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
2463                // §Fase 91.a — the frame's cognitive timezone (IANA string).
2464                "now" => node.now_tz = Some(self.consume(TokenType::StringLit)?.value),
2465                "max_tokens" => {
2466                    node.max_tokens = Some(
2467                        self.consume(TokenType::Integer)?
2468                            .value
2469                            .parse::<i64>()
2470                            .unwrap_or(0),
2471                    )
2472                }
2473                "temperature" => node.temperature = Some(self.consume_number()?),
2474                "cite_sources" => node.cite_sources = Some(self.parse_bool()?),
2475                _ => self.skip_value(),
2476            }
2477        }
2478        self.consume(TokenType::RBrace)?;
2479        Ok(node)
2480    }
2481
2482    // ── ANCHOR ───────────────────────────────────────────────────
2483
2484    fn parse_anchor(&mut self) -> Result<AnchorConstraint, ParseError> {
2485        let tok = self.consume(TokenType::Anchor)?;
2486        let loc = self.loc_of(&tok);
2487        let name = self.consume(TokenType::Identifier)?.value;
2488        self.consume(TokenType::LBrace)?;
2489
2490        let mut node = AnchorConstraint {
2491            name,
2492            require: String::new(),
2493            reject: Vec::new(),
2494            enforce: String::new(),
2495            description: String::new(),
2496            confidence_floor: None,
2497            unknown_response: String::new(),
2498            on_violation: String::new(),
2499            on_violation_target: String::new(),
2500            loc,
2501            leading_trivia: Vec::new(),
2502            trailing_trivia: Vec::new(),
2503        };
2504
2505        while !self.check(TokenType::RBrace) {
2506            let field_name = self.current().value.clone();
2507            self.advance();
2508            self.consume(TokenType::Colon)?;
2509
2510            match field_name.as_str() {
2511                "require" => node.require = self.consume_any_ident_or_kw()?.value,
2512                "description" => node.description = self.consume(TokenType::StringLit)?.value,
2513                "reject" => node.reject = self.parse_bracketed_identifiers()?,
2514                "enforce" => node.enforce = self.consume_any_ident_or_kw()?.value,
2515                "confidence_floor" => node.confidence_floor = Some(self.consume_number()?),
2516                "unknown_response" => {
2517                    node.unknown_response = self.consume(TokenType::StringLit)?.value
2518                }
2519                "on_violation" => {
2520                    // Parse: raise ErrorName | fallback(...) | identifier
2521                    let action = self.consume_any_ident_or_kw()?.value;
2522                    node.on_violation = action.clone();
2523                    if action == "raise" || action == "fallback" {
2524                        node.on_violation_target = self.consume_any_ident_or_kw()?.value;
2525                    }
2526                }
2527                _ => self.skip_value(),
2528            }
2529        }
2530        self.consume(TokenType::RBrace)?;
2531        Ok(node)
2532    }
2533
2534    // ── MEMORY ───────────────────────────────────────────────────
2535
2536    fn parse_memory(&mut self) -> Result<MemoryDefinition, ParseError> {
2537        let tok = self.consume(TokenType::Memory)?;
2538        let loc = self.loc_of(&tok);
2539        let name = self.consume(TokenType::Identifier)?.value;
2540        self.consume(TokenType::LBrace)?;
2541
2542        let mut node = MemoryDefinition {
2543            name,
2544            store: String::new(),
2545            backend: String::new(),
2546            retrieval: String::new(),
2547            decay: String::new(),
2548            loc,
2549            leading_trivia: Vec::new(),
2550            trailing_trivia: Vec::new(),
2551        };
2552
2553        while !self.check(TokenType::RBrace) {
2554            let field_name = self.current().value.clone();
2555            self.advance();
2556            self.consume(TokenType::Colon)?;
2557
2558            match field_name.as_str() {
2559                "store" => node.store = self.consume_any_ident_or_kw()?.value,
2560                "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
2561                "retrieval" => node.retrieval = self.consume_any_ident_or_kw()?.value,
2562                "decay" => {
2563                    if self.check(TokenType::Duration) {
2564                        node.decay = self.advance().value.clone();
2565                    } else {
2566                        node.decay = self.consume_any_ident_or_kw()?.value;
2567                    }
2568                }
2569                _ => self.skip_value(),
2570            }
2571        }
2572        self.consume(TokenType::RBrace)?;
2573        Ok(node)
2574    }
2575
2576    // ── TOOL ─────────────────────────────────────────────────────
2577
2578    fn parse_tool(&mut self) -> Result<ToolDefinition, ParseError> {
2579        let tok = self.consume(TokenType::Tool)?;
2580        let loc = self.loc_of(&tok);
2581        let name = self.consume(TokenType::Identifier)?.value;
2582        self.consume(TokenType::LBrace)?;
2583
2584        let mut node = ToolDefinition {
2585            name,
2586            provider: String::new(),
2587            max_results: None,
2588            filter_expr: String::new(),
2589            timeout: String::new(),
2590            runtime: String::new(),
2591            resource_ref: String::new(),
2592            sandbox: None,
2593            effects: None,
2594            parameters: Vec::new(),
2595            output_type: None,
2596            requires: Vec::new(),
2597            secret: String::new(),
2598            secret_partition: String::new(),
2599            target: None,
2600            risk: None,
2601            argv: Vec::new(),
2602            cache: String::new(),
2603            scrape: None,
2604            loc,
2605            leading_trivia: Vec::new(),
2606            trailing_trivia: Vec::new(),
2607        };
2608
2609        // §Fase 84.b/D84.13 — unknown fields are recorded (not silently
2610        // skipped) so a `target:`-bound technician tool can HARD-ERROR on one
2611        // (a typo'd safety field must never quietly disable a guard), while a
2612        // legacy schema-less tool keeps its lenient record-and-skip (zero
2613        // regression). The decision is deferred to after the block is parsed,
2614        // since `target:` may appear after the unknown field.
2615        let mut unknown_fields: Vec<(String, u32, u32)> = Vec::new();
2616
2617        while !self.check(TokenType::RBrace) {
2618            let field_tok = self.current().clone();
2619            let field_name = field_tok.value.clone();
2620            self.advance();
2621            self.consume(TokenType::Colon)?;
2622
2623            match field_name.as_str() {
2624                "provider" => node.provider = self.consume_any_ident_or_kw()?.value,
2625                "max_results" => {
2626                    node.max_results = Some(
2627                        self.consume(TokenType::Integer)?
2628                            .value
2629                            .parse::<i64>()
2630                            .unwrap_or(0),
2631                    )
2632                }
2633                "filter" => node.filter_expr = self.parse_filter_expression()?,
2634                "timeout" => node.timeout = self.consume(TokenType::Duration)?.value,
2635                "runtime" => node.runtime = self.consume_any_ident_or_kw()?.value,
2636                // §Fase 114.c — the `resource` this tool's channel runs on. The
2637                // channel's address, concurrency and lifecycle come from it;
2638                // `runtime:` then names the path within the channel.
2639                "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
2640                "sandbox" => node.sandbox = Some(self.parse_bool()?),
2641                "effects" => node.effects = Some(self.parse_effect_row()?),
2642                // §Fase 58.a — the tool's typed input schema + output type.
2643                "parameters" => node.parameters = self.parse_tool_param_schema()?,
2644                "output_type" => node.output_type = Some(self.parse_output_type_string()?),
2645                // §Fase 116.a (D116.9) — the tool's required authorization
2646                // scopes: bare dot-separated capability slugs, the EXACT
2647                // grammar + charset of `credential.grants` (§92) so the two
2648                // vocabularies are one. `requires: [w_organization_social,
2649                // video.publish]`. Subset coverage is `axon-T956`.
2650                "requires" => {
2651                    let bracket_tok = self.current().clone();
2652                    let items = self.parse_bracketed_dot_identifiers()?;
2653                    for slug in &items {
2654                        if !is_valid_capability_slug(slug) {
2655                            return Err(ParseError {
2656                                message: format!(
2657                                    "Invalid capability slug '{slug}' in tool '{}' \
2658                                     `requires:`. Scope slugs must match \
2659                                     ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — the same \
2660                                     grammar as `credential.grants`. Examples: \
2661                                     `w_organization_social`, `video.publish`.",
2662                                    node.name
2663                                ),
2664                                line: bracket_tok.line,
2665                                column: bracket_tok.column,
2666                                ..Default::default()
2667                            });
2668                        }
2669                    }
2670                    node.requires = items;
2671                }
2672                // §Fase 94.c — the per-tenant secret KEY injected at
2673                // dispatch (`rotation_without_revelation`). Key shape +
2674                // technician exclusion are `axon-T902` (type-checker).
2675                "secret" => node.secret = self.parse_dotted_identifier()?,
2676                // §Fase 95.a — `secret_partition:` names one of this tool's
2677                // own `parameters:` (a bare identifier, NOT dotted — it is a
2678                // parameter reference, not a key). Its runtime value becomes
2679                // a single appended key segment at dispatch. The membership +
2680                // `String`-type + technician laws are `axon-T903`.
2681                "secret_partition" => {
2682                    node.secret_partition = self.consume_any_ident_or_kw()?.value
2683                }
2684                // §Fase 84.b — Remote Hands technician fields.
2685                "target" => node.target = Some(self.consume_any_ident_or_kw()?.value),
2686                "risk" => node.risk = Some(self.consume_any_ident_or_kw()?.value),
2687                // The argv template: a bracketed list of quoted elements
2688                // (`argv: ["ping", "-c", "${count}", "${host}"]`). Reuses the
2689                // CORS list helper (tolerant of `[]` and a trailing comma).
2690                "argv" => node.argv = self.parse_bracketed_strings()?,
2691                // §Fase 85.b — the tool's result-memoization policy reference
2692                // (a declared `cache` name, or the `none` opt-out sentinel).
2693                "cache" => node.cache = self.consume_any_ident_or_kw()?.value,
2694                // §Fase 98.b — the closed-catalog web-acquisition config
2695                // block. `scrape: { engine: …, extract: […], … }`.
2696                "scrape" => node.scrape = Some(self.parse_scrape_spec()?),
2697                _ => {
2698                    unknown_fields.push((field_name, field_tok.line, field_tok.column));
2699                    self.skip_value();
2700                }
2701            }
2702        }
2703        self.consume(TokenType::RBrace)?;
2704
2705        // §Fase 84.b/D84.13 — a `target:`-bound tool opts into strict field
2706        // checking. An unknown field on it is a parse error, mirroring the §83
2707        // `cors`/`voice` closed-catalog discipline — but scoped to the
2708        // technician surface so ordinary tools are untouched.
2709        // §Fase 98.b (D98.2) — a `scrape:`-bearing web-acquisition tool opts
2710        // into the same strictness: a typo'd safety field (e.g. a mis-spelled
2711        // `respect_robots`) must never quietly disable a guard.
2712        if node.target.is_some() || node.scrape.is_some() {
2713            if let Some((field_name, line, column)) = unknown_fields.into_iter().next() {
2714                let (surface, valid) = if node.target.is_some() {
2715                    (
2716                        "technician tool (§Fase 84 D84.13)",
2717                        "provider, parameters, output_type, timeout, effects, target, risk, argv",
2718                    )
2719                } else {
2720                    (
2721                        "web-acquisition tool (§Fase 98 D98.2)",
2722                        "provider, parameters, output_type, timeout, effects, secret, \
2723                         secret_partition, cache, scrape",
2724                    )
2725                };
2726                return Err(ParseError {
2727                    message: format!(
2728                        "unknown field `{field_name}` in {surface} `{}` — this tool uses \
2729                         strict field checking; valid fields: {valid}",
2730                        node.name
2731                    ),
2732                    line,
2733                    column,
2734                    ..Default::default()
2735                });
2736            }
2737        }
2738        Ok(node)
2739    }
2740
2741    /// §Fase 98.b — parse the closed-catalog `scrape: { … }` web-acquisition
2742    /// config sub-block. Every field is optional; an unknown field is a hard
2743    /// parse error (the §83 `cors` closed-catalog discipline). Mirrors the
2744    /// field grammar of `parse_tool` for the scrape-specific keys.
2745    fn parse_scrape_spec(&mut self) -> Result<crate::ast::ScrapeSpec, ParseError> {
2746        let open = self.consume(TokenType::LBrace)?;
2747        let loc = self.loc_of(&open);
2748        let mut spec = crate::ast::ScrapeSpec {
2749            loc,
2750            ..Default::default()
2751        };
2752        while !self.check(TokenType::RBrace) {
2753            let field_tok = self.current().clone();
2754            let field_name = field_tok.value.clone();
2755            self.advance();
2756            self.consume(TokenType::Colon)?;
2757            match field_name.as_str() {
2758                "engine" => spec.engine = Some(self.consume_any_ident_or_kw()?.value),
2759                "impersonate" => spec.impersonate = Some(self.consume_any_ident_or_kw()?.value),
2760                "render_wait" => spec.render_wait = Some(self.consume(TokenType::Duration)?.value),
2761                "proxy" => spec.proxy = self.parse_dotted_identifier()?,
2762                "respect_robots" => spec.respect_robots = Some(self.parse_bool()?),
2763                "extract" => spec.extract = self.parse_bracketed_strings()?,
2764                "adaptive" => spec.adaptive = Some(self.parse_bool()?),
2765                "similarity_floor" => spec.similarity_floor = self.parse_optional_float(),
2766                "follow" => spec.follow = self.consume(TokenType::StringLit)?.value,
2767                "max_depth" => {
2768                    spec.max_depth =
2769                        Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2770                }
2771                "max_pages" => {
2772                    spec.max_pages =
2773                        Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2774                }
2775                "concurrency" => {
2776                    spec.concurrency =
2777                        Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2778                }
2779                "politeness" => spec.politeness = self.consume_any_ident_or_kw()?.value,
2780                "checkpoint" => spec.checkpoint = self.consume_any_ident_or_kw()?.value,
2781                other => {
2782                    return Err(self.error(&format!(
2783                        "unknown scrape field `{other}` — the `scrape: {{ … }}` block is a \
2784                         closed catalog (§Fase 98 D98.2); valid fields: engine, impersonate, \
2785                         render_wait, proxy, respect_robots, extract, adaptive, \
2786                         similarity_floor, follow, max_depth, max_pages, concurrency, \
2787                         politeness, checkpoint"
2788                    )));
2789                }
2790            }
2791        }
2792        self.consume(TokenType::RBrace)?;
2793        Ok(spec)
2794    }
2795
2796    /// §Fase 58.a — parse a tool's INPUT SCHEMA: a brace-delimited list of
2797    /// `name: Type` parameters (`parameters: { query: String, max_results: Int }`).
2798    /// Reuses the flow-parameter shape (`Parameter`), so the same `TypeExpr`
2799    /// grammar — generics like `List<T>`, `?`-optionals — applies. A trailing
2800    /// comma is tolerated; an empty `{}` yields no parameters.
2801    fn parse_tool_param_schema(&mut self) -> Result<Vec<Parameter>, ParseError> {
2802        self.consume(TokenType::LBrace)?;
2803        let mut params = Vec::new();
2804        while !self.check(TokenType::RBrace) {
2805            // Accept a keyword-as-name (`filter`, `type`, `domain`, …) — real
2806            // adopter tool schemas use such parameter names; the `:` after it
2807            // disambiguates.
2808            let name = self.consume_any_ident_or_kw()?;
2809            let ploc = self.loc_of(&name);
2810            self.consume(TokenType::Colon)?;
2811            let type_expr = self.parse_type_expr()?;
2812            params.push(Parameter {
2813                name: name.value,
2814                type_expr,
2815                loc: ploc,
2816            });
2817            if self.check(TokenType::Comma) {
2818                self.advance();
2819            } else {
2820                break;
2821            }
2822        }
2823        self.consume(TokenType::RBrace)?;
2824        Ok(params)
2825    }
2826
2827    fn parse_filter_expression(&mut self) -> Result<String, ParseError> {
2828        let name = self.consume_any_ident_or_kw()?.value;
2829        if self.check(TokenType::LParen) {
2830            self.advance();
2831            let mut parts = vec![name, "(".to_string()];
2832            while !self.check(TokenType::RParen) {
2833                parts.push(self.advance().value.clone());
2834            }
2835            self.consume(TokenType::RParen)?;
2836            parts.push(")".to_string());
2837            Ok(parts.join(""))
2838        } else {
2839            Ok(name)
2840        }
2841    }
2842
2843    fn parse_effect_row(&mut self) -> Result<EffectRow, ParseError> {
2844        let tok = self.consume(TokenType::Lt)?;
2845        let loc = self.loc_of(&tok);
2846        let mut effects = Vec::new();
2847        let mut epistemic_level = String::new();
2848
2849        while !self.check(TokenType::Gt) {
2850            let name = self.consume_any_ident_or_kw()?.value;
2851            if self.check(TokenType::Colon) {
2852                self.advance();
2853                // Fase 11.c / 11.e — qualifiers can be compound slugs
2854                // from a closed catalogue:
2855                //
2856                //   * dot-separated  — `legal:HIPAA.164_502`,
2857                //                       `legal:GDPR.Art6.Consent`,
2858                //                       `legal:PCI_DSS.v4_Req3`
2859                //   * colon-separated — `ots:transform:mulaw8:pcm16`,
2860                //                       `ots:backend:native`
2861                //   * mixed           — supported by the same loop.
2862                //
2863                // The lexer fragments dotted slugs across IDENT /
2864                // INTEGER tokens (e.g., `164_502` lexes as INTEGER
2865                // `164` + IDENT `_502` because `_` starts a fresh
2866                // identifier); we recombine here using source-column
2867                // adjacency so the type checker sees the catalog
2868                // string verbatim.
2869                let level = self.parse_qualifier_value()?;
2870                if name == "epistemic" {
2871                    epistemic_level = level;
2872                } else {
2873                    effects.push(format!("{name}:{level}"));
2874                }
2875            } else {
2876                effects.push(name);
2877            }
2878            if self.check(TokenType::Comma) {
2879                self.advance();
2880            }
2881        }
2882        self.consume(TokenType::Gt)?;
2883
2884        Ok(EffectRow {
2885            effects,
2886            epistemic_level,
2887            loc,
2888        })
2889    }
2890
2891    /// Parse a compound qualifier value following an effect's first
2892    /// colon — supports both dot-separated (`HIPAA.164_502`) and
2893    /// colon-separated (`transform:mulaw8:pcm16`) catalogue slugs, as
2894    /// well as mixed forms.
2895    ///
2896    /// The grammar is: `segment ((`.` | `:`) segment)*` where a
2897    /// segment is a contiguous run of IDENT / INTEGER tokens (see
2898    /// [`Self::consume_dotted_slug_segment`]).
2899    fn parse_qualifier_value(&mut self) -> Result<String, ParseError> {
2900        let mut buf = self.consume_dotted_slug_segment()?;
2901        loop {
2902            let sep = if self.check(TokenType::Dot) {
2903                '.'
2904            } else if self.check(TokenType::Colon) {
2905                ':'
2906            } else {
2907                break;
2908            };
2909            self.advance();
2910            let part = self.consume_dotted_slug_segment()?;
2911            buf.push(sep);
2912            buf.push_str(&part);
2913        }
2914        Ok(buf)
2915    }
2916
2917    /// Consume a contiguous run of IDENT / INTEGER / keyword-ident
2918    /// tokens whose source positions are adjacent (no whitespace
2919    /// between them), concatenating their text into a single segment.
2920    ///
2921    /// Needed for closed-catalogue qualifier slugs whose segment
2922    /// mixes digits and identifier characters — e.g. `HIPAA.164_502`
2923    /// lexes as INTEGER `164` + IDENT `_502` because `_` starts a
2924    /// fresh identifier; the catalog value is the concatenation
2925    /// `164_502`. Adjacency is determined by matching
2926    /// `(line, column + len)` of the previous token against the next
2927    /// token's start position.
2928    fn consume_dotted_slug_segment(&mut self) -> Result<String, ParseError> {
2929        let first = self.consume_any_ident_or_kw()?;
2930        let mut buf = first.value.clone();
2931        let mut next_line = first.line;
2932        let mut next_col = first.column + first.value.chars().count() as u32;
2933        loop {
2934            let cur = self.current();
2935            let is_segment_token = matches!(cur.ttype, TokenType::Identifier | TokenType::Integer,);
2936            if !is_segment_token {
2937                break;
2938            }
2939            if cur.line != next_line || cur.column != next_col {
2940                break;
2941            }
2942            buf.push_str(&cur.value);
2943            next_col = cur.column + cur.value.chars().count() as u32;
2944            next_line = cur.line;
2945            self.pos += 1;
2946        }
2947        Ok(buf)
2948    }
2949
2950    // ── TYPE ─────────────────────────────────────────────────────
2951
2952    fn parse_type_def(&mut self) -> Result<TypeDefinition, ParseError> {
2953        let tok = self.consume(TokenType::Type)?;
2954        let loc = self.loc_of(&tok);
2955        let name = self.consume(TokenType::Identifier)?.value;
2956
2957        let mut node = TypeDefinition {
2958            name,
2959            fields: Vec::new(),
2960            range_constraint: None,
2961            where_clause: None,
2962            compliance: Vec::new(),
2963            loc: loc.clone(),
2964            leading_trivia: Vec::new(),
2965            trailing_trivia: Vec::new(),
2966        };
2967
2968        // Optional range: (0.0..1.0)
2969        if self.check(TokenType::LParen) {
2970            self.advance();
2971            let min_val = self.consume_number()?;
2972            self.consume(TokenType::DotDot)?;
2973            let max_val = self.consume_number()?;
2974            self.consume(TokenType::RParen)?;
2975            node.range_constraint = Some(RangeConstraint {
2976                min_value: min_val,
2977                max_value: max_val,
2978                loc: loc.clone(),
2979            });
2980        }
2981
2982        // Optional where clause
2983        if self.check(TokenType::Where) {
2984            self.advance();
2985            let mut expr_parts = Vec::new();
2986            while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
2987                if self.check(TokenType::Eof) {
2988                    break;
2989                }
2990                expr_parts.push(self.advance().value.clone());
2991            }
2992            node.where_clause = Some(WhereClause {
2993                expression: expr_parts.join(" "),
2994                loc: loc.clone(),
2995            });
2996        }
2997
2998        // Optional ESK Fase 6.1 — `compliance [HIPAA, ...]` prefix modifier
2999        // between `type Name` / `range` / `where` and the body `{`.
3000        if self.check(TokenType::Identifier) && self.current().value == "compliance" {
3001            self.advance();
3002            node.compliance = self.parse_bracketed_identifiers()?;
3003        }
3004
3005        // Optional body: { field: Type, ... }
3006        if self.check(TokenType::LBrace) {
3007            self.advance();
3008            while !self.check(TokenType::RBrace) {
3009                let field_name = self.consume(TokenType::Identifier)?;
3010                let field_loc = self.loc_of(&field_name);
3011                self.consume(TokenType::Colon)?;
3012                let type_expr = self.parse_type_expr()?;
3013                node.fields.push(TypeField {
3014                    name: field_name.value,
3015                    type_expr,
3016                    loc: field_loc,
3017                });
3018                if self.check(TokenType::Comma) {
3019                    self.advance();
3020                }
3021            }
3022            self.consume(TokenType::RBrace)?;
3023        }
3024
3025        Ok(node)
3026    }
3027
3028    fn parse_type_expr(&mut self) -> Result<TypeExpr, ParseError> {
3029        // §Fase 119.c — a LEADING bracket is the list-type sugar the README
3030        // has always written in flow signatures: `readings: [SensorReading]`
3031        // (blocks 44-45). It lowers to exactly what `List<SensorReading>`
3032        // produces, so nothing downstream learns a new shape — the §39.a
3033        // comment below already names `List<T>` as the canonical carrier.
3034        if self.check(TokenType::LBracket) {
3035            let open = self.current().clone();
3036            self.advance();
3037            let inner = self.parse_type_expr()?;
3038            self.consume(TokenType::RBracket)?;
3039            let mut optional = false;
3040            if self.check(TokenType::Question) {
3041                self.advance();
3042                optional = true;
3043            }
3044            return Ok(TypeExpr {
3045                name: "List".to_string(),
3046                generic_param: if inner.generic_param.is_empty() {
3047                    inner.name
3048                } else {
3049                    format!("{}<{}>", inner.name, inner.generic_param)
3050                },
3051                optional,
3052                loc: self.loc_of(&open),
3053            });
3054        }
3055        let name_tok = self.consume(TokenType::Identifier)?;
3056        let loc = self.loc_of(&name_tok);
3057        let mut generic_param = String::new();
3058        let mut optional = false;
3059
3060        if self.check(TokenType::Lt) {
3061            self.advance();
3062            // §Fase 39.a — recursive: the generic param can itself be a
3063            // nested type expression. `FlowEnvelope<List<TenantRecord>>`
3064            // parses as outer=FlowEnvelope, inner=List<TenantRecord>.
3065            // Pre-39.a the inner had to be a single Identifier; nested
3066            // generics like the canonical FlowEnvelope<T> wrapper
3067            // required this lift. Backwards-compat preserved for
3068            // single-level generics like `Stream<Token>` and
3069            // `List<T>` — the recursion lands once and returns the
3070            // same flat string the v1.x parser produced.
3071            let inner = self.parse_type_expr()?;
3072            generic_param = if inner.generic_param.is_empty() {
3073                inner.name
3074            } else {
3075                format!("{}<{}>", inner.name, inner.generic_param)
3076            };
3077            self.consume(TokenType::Gt)?;
3078        }
3079        // §Fase 51.c.3 — bracket type parameters for the continuous-carrier
3080        // grammar: `SymbolicPtr[Tensor[Float32]]`, `DensityMatrix[1024]`. The
3081        // param is either a nested type expression OR a numeric dimension.
3082        if self.check(TokenType::LBracket) {
3083            self.advance();
3084            if matches!(self.current().ttype, TokenType::Integer | TokenType::Float) {
3085                generic_param = self.advance().value.clone();
3086            } else {
3087                let inner = self.parse_type_expr()?;
3088                generic_param = if inner.generic_param.is_empty() {
3089                    inner.name
3090                } else {
3091                    format!("{}[{}]", inner.name, inner.generic_param)
3092                };
3093            }
3094            self.consume(TokenType::RBracket)?;
3095        }
3096        if self.check(TokenType::Question) {
3097            self.advance();
3098            optional = true;
3099        }
3100
3101        Ok(TypeExpr {
3102            name: name_tok.value,
3103            generic_param,
3104            optional,
3105            loc,
3106        })
3107    }
3108
3109    /// Parse a type expression in a context where the AST stores the
3110    /// shape as a flat string (step / reason / forge / ots-apply
3111    /// productions). Mirrors Python `_parse_output_type_string`.
3112    ///
3113    /// Accepts:
3114    /// - `Identifier`        → `"Identifier"`
3115    /// - `Stream<String>`    → `"Stream<String>"`
3116    /// - `Optional?`         → `"Optional?"`
3117    /// - `Stream<String>?`   → `"Stream<String>?"`
3118    ///
3119    /// **Why this exists** — pre-fix, the step parser called
3120    /// `consume(TokenType::Identifier)?.value` which captured only
3121    /// the head identifier and left `< … >` unconsumed. For
3122    /// `output: Stream<Token>`, this produced `output_type =
3123    /// "Stream"`, and downstream `flow_has_stream_output`'s
3124    /// `starts_with("Stream<") && ends_with('>')` predicate then
3125    /// returned false → `implicit_transport == "json"` → the
3126    /// dynamic-route fallback in `axon-rs` served JSON instead of
3127    /// SSE even when the adopter's source canonically declared the
3128    /// algebraic stream effect. Surfaced 2026-05-12 by adopter
3129    /// `docs/MIGRATION_TO_AXON.md` audit after the v1.23.0 wire-
3130    /// layer didn't honor the declarative effect. Python parser was
3131    /// fixed for the same gap 2026-05-09; this is the Rust cross-
3132    /// stack catch-up.
3133    fn parse_output_type_string(&mut self) -> Result<String, ParseError> {
3134        let expr = self.parse_type_expr()?;
3135        let mut s = expr.name;
3136        if !expr.generic_param.is_empty() {
3137            s.push('<');
3138            s.push_str(&expr.generic_param);
3139            s.push('>');
3140        }
3141        if expr.optional {
3142            s.push('?');
3143        }
3144        Ok(s)
3145    }
3146
3147    // ── FLOW ─────────────────────────────────────────────────────
3148
3149    fn parse_flow(&mut self) -> Result<FlowDefinition, ParseError> {
3150        let tok = self.consume(TokenType::Flow)?;
3151        let loc = self.loc_of(&tok);
3152        let name = self.consume(TokenType::Identifier)?.value;
3153
3154        self.consume(TokenType::LParen)?;
3155        let mut parameters = Vec::new();
3156        if !self.check(TokenType::RParen) {
3157            parameters = self.parse_param_list()?;
3158        }
3159        self.consume(TokenType::RParen)?;
3160
3161        let mut return_type = None;
3162        if self.check(TokenType::Arrow) {
3163            self.advance();
3164            return_type = Some(self.parse_type_expr()?);
3165        }
3166
3167        self.consume(TokenType::LBrace)?;
3168        let mut body = Vec::new();
3169        while !self.check(TokenType::RBrace) {
3170            body.push(self.parse_flow_step()?);
3171        }
3172        self.consume(TokenType::RBrace)?;
3173
3174        Ok(FlowDefinition {
3175            name,
3176            parameters,
3177            return_type,
3178            body,
3179            loc,
3180            leading_trivia: Vec::new(),
3181            trailing_trivia: Vec::new(),
3182        })
3183    }
3184
3185    // ── §Fase 120 — algebraic effects (Plotkin/Pretnar) ─────────────
3186    //
3187    // Four constructs, in the shape `fase_23` §3.1 publishes verbatim.
3188
3189    /// `effect SSE { Emit(token: Token) -> Unit  Done() -> Never }`
3190    ///
3191    /// The declaration exists so the operation catalog is CLOSED. D120.2's bare
3192    /// `perform Emit(x)` resolves against exactly this set, and an operation
3193    /// two effects both declare is a compile error naming both — not a silent
3194    /// pick. Without the declaration there would be nothing to resolve against
3195    /// and `effect_name` would be a free string, which is the defect
3196    /// `feedback_free_string_field_breeds_fake_catalog` names.
3197    fn parse_effect(&mut self) -> Result<EffectDefinition, ParseError> {
3198        let tok = self.consume(TokenType::Effect)?;
3199        let loc = self.loc_of(&tok);
3200        let name = self.consume_any_ident_or_kw()?.value;
3201        self.consume(TokenType::LBrace)?;
3202
3203        let mut operations: Vec<EffectOperation> = Vec::new();
3204        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3205            let op_tok = self.current().clone();
3206            let op_name = self.consume_any_ident_or_kw()?.value;
3207
3208            self.consume(TokenType::LParen)?;
3209            let parameters = if self.check(TokenType::RParen) {
3210                Vec::new()
3211            } else {
3212                self.parse_param_list()?
3213            };
3214            self.consume(TokenType::RParen)?;
3215
3216            // `-> T` is optional in the grammar; §3.1 always writes it, and a
3217            // missing return type reads as Unit at the type-checker.
3218            let mut return_type = String::new();
3219            if self.check(TokenType::Arrow) {
3220                self.advance();
3221                return_type = self.parse_type_expr()?.name;
3222            }
3223
3224            // A duplicate operation name inside ONE effect is refused: the
3225            // handler-clause lookup is by operation name, so two declarations
3226            // would make the arity check depend on which one the search found
3227            // first — a defect nobody would ever see fire.
3228            if let Some(prior) = operations.iter().find(|o| o.name == op_name) {
3229                return Err(ParseError {
3230                    message: format!(
3231                        "effect `{name}` declares operation `{op_name}` twice (first at \
3232                         line {}); handler dispatch is by operation NAME, so a second \
3233                         declaration would silently shadow the first",
3234                        prior.loc.line
3235                    ),
3236                    line: op_tok.line,
3237                    column: op_tok.column,
3238                    ..Default::default()
3239                });
3240            }
3241
3242            operations.push(EffectOperation {
3243                name: op_name,
3244                parameters,
3245                return_type,
3246                loc: self.loc_of(&op_tok),
3247            });
3248        }
3249        self.consume(TokenType::RBrace)?;
3250
3251        Ok(EffectDefinition {
3252            name,
3253            operations,
3254            loc,
3255            leading_trivia: Vec::new(),
3256            trailing_trivia: Vec::new(),
3257        })
3258    }
3259
3260    /// `handle SSE { Emit(token) -> { … } } in { … }` — the delimited handler
3261    /// scope (D3).
3262    ///
3263    /// The `in { … }` body is parsed with [`Self::parse_flow_step`], and that is
3264    /// the whole point of D120.1: the body is ORDINARY flow steps, so
3265    /// `run generate(…)` inside a handler runs for real. Lowering it onto
3266    /// `axon-rs`'s `Instruction` alphabet instead would have made every
3267    /// non-effect node in it a `Passthrough` — inert — which is the §111 defect
3268    /// this fase exists not to repeat.
3269    fn parse_handle_block(&mut self) -> Result<HandleBlock, ParseError> {
3270        let tok = self.consume(TokenType::Handle)?;
3271        let loc = self.loc_of(&tok);
3272
3273        // `handle E1, E2 { … }` — one frame may intercept several effects.
3274        let mut effect_names = vec![self.consume_any_ident_or_kw()?.value];
3275        while self.check(TokenType::Comma) {
3276            self.advance();
3277            effect_names.push(self.consume_any_ident_or_kw()?.value);
3278        }
3279
3280        self.consume(TokenType::LBrace)?;
3281        let mut clauses: Vec<HandlerClause> = Vec::new();
3282        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3283            let clause_tok = self.current().clone();
3284            let operation_name = self.consume_any_ident_or_kw()?.value;
3285
3286            // Clause binders are BARE names — `Emit(token) -> { … }`. The types
3287            // live on the effect declaration; repeating them here would let the
3288            // two disagree.
3289            self.consume(TokenType::LParen)?;
3290            let mut parameter_names = Vec::new();
3291            while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
3292                parameter_names.push(self.consume_any_ident_or_kw()?.value);
3293                if self.check(TokenType::Comma) {
3294                    self.advance();
3295                }
3296            }
3297            self.consume(TokenType::RParen)?;
3298            self.consume(TokenType::Arrow)?;
3299            self.consume(TokenType::LBrace)?;
3300
3301            let mut body = Vec::new();
3302            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3303                body.push(self.parse_flow_step()?);
3304            }
3305            self.consume(TokenType::RBrace)?;
3306
3307            if let Some(prior) = clauses.iter().find(|c| c.operation_name == operation_name) {
3308                return Err(ParseError {
3309                    message: format!(
3310                        "handler declares clause `{operation_name}` twice (first at line {}); \
3311                         dispatch finds a clause by operation NAME and would always run the \
3312                         first, leaving the second dead",
3313                        prior.loc.line
3314                    ),
3315                    line: clause_tok.line,
3316                    column: clause_tok.column,
3317                    ..Default::default()
3318                });
3319            }
3320
3321            clauses.push(HandlerClause {
3322                operation_name,
3323                parameter_names,
3324                body,
3325                loc: self.loc_of(&clause_tok),
3326            });
3327        }
3328        self.consume(TokenType::RBrace)?;
3329
3330        // The `in { … }` delimiter is MANDATORY. A `handle` without it declares
3331        // a scope with no extent — nothing could ever be intercepted by it, and
3332        // accepting it would let an author believe an effect was handled when
3333        // no `perform` is inside anything.
3334        let in_tok = self.current().clone();
3335        if !self.check(TokenType::In) {
3336            return Err(ParseError {
3337                message: format!(
3338                    "`handle {}` must be followed by `in {{ … }}` — a handler scope is \
3339                     DELIMITED (fase_23 D3). Without the `in` block the frame has no \
3340                     extent, so no `perform` could ever reach these clauses (got '{}')",
3341                    effect_names.join(", "),
3342                    in_tok.value
3343                ),
3344                line: in_tok.line,
3345                column: in_tok.column,
3346                ..Default::default()
3347            });
3348        }
3349        self.advance();
3350        self.consume(TokenType::LBrace)?;
3351        let mut body = Vec::new();
3352        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3353            body.push(self.parse_flow_step()?);
3354        }
3355        self.consume(TokenType::RBrace)?;
3356
3357        Ok(HandleBlock {
3358            effect_names,
3359            clauses,
3360            body,
3361            loc,
3362        })
3363    }
3364
3365    /// The shared head of `perform` and `forward` (D12): an optionally
3366    /// qualified operation name plus a parenthesised argument list.
3367    ///
3368    /// D120.2 — BOTH spellings parse. `SSE.Emit(x)` fixes the effect here;
3369    /// `Emit(x)` leaves `effect_name` `None` and the closed catalog resolves it
3370    /// downstream, where an ambiguity can be reported with both candidates
3371    /// named. The qualified form is told from the bare one by the `.`, which
3372    /// cannot appear in an operation name.
3373    fn parse_effect_op_ref(
3374        &mut self,
3375    ) -> Result<(Option<String>, String, Vec<String>), ParseError> {
3376        let first = self.consume_any_ident_or_kw()?.value;
3377        let (effect_name, operation_name) = if self.check(TokenType::Dot) {
3378            self.advance();
3379            (Some(first), self.consume_any_ident_or_kw()?.value)
3380        } else {
3381            (None, first)
3382        };
3383
3384        self.consume(TokenType::LParen)?;
3385        let mut arguments = Vec::new();
3386        while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
3387            // §119.f.10 SUBJECTS: `fase_23` §3.1 writes `perform
3388            // Emit(response.token)` — a dotted reference into a prior binding.
3389            arguments.push(self.parse_subject()?);
3390            if self.check(TokenType::Comma) {
3391                self.advance();
3392            }
3393        }
3394        self.consume(TokenType::RParen)?;
3395        Ok((effect_name, operation_name, arguments))
3396    }
3397
3398    /// `perform Emit(x)` / `perform SSE.Emit(x)`.
3399    fn parse_perform_step(&mut self) -> Result<PerformStep, ParseError> {
3400        let tok = self.consume(TokenType::Perform)?;
3401        let (effect_name, operation_name, arguments) = self.parse_effect_op_ref()?;
3402        Ok(PerformStep {
3403            effect_name,
3404            operation_name,
3405            arguments,
3406            loc: self.loc_of(&tok),
3407        })
3408    }
3409
3410    /// `forward Emit(t)` / `forward SSE.Emit(t)` (D12).
3411    fn parse_forward_step(&mut self) -> Result<ForwardStep, ParseError> {
3412        let tok = self.consume(TokenType::Forward)?;
3413        let (effect_name, operation_name, arguments) = self.parse_effect_op_ref()?;
3414        Ok(ForwardStep {
3415            effect_name,
3416            operation_name,
3417            arguments,
3418            loc: self.loc_of(&tok),
3419        })
3420    }
3421
3422    /// The shared body of `resume(…)` / `abort(…)`: an optional single value.
3423    fn parse_discharge_value(&mut self) -> Result<String, ParseError> {
3424        self.consume(TokenType::LParen)?;
3425        let value = if self.check(TokenType::RParen) {
3426            String::new()
3427        } else {
3428            self.parse_subject()?
3429        };
3430        self.consume(TokenType::RParen)?;
3431        Ok(value)
3432    }
3433
3434    fn parse_param_list(&mut self) -> Result<Vec<Parameter>, ParseError> {
3435        let mut params = Vec::new();
3436
3437        let name = self.consume(TokenType::Identifier)?;
3438        let ploc = self.loc_of(&name);
3439        self.consume(TokenType::Colon)?;
3440        let type_expr = self.parse_type_expr()?;
3441        params.push(Parameter {
3442            name: name.value,
3443            type_expr,
3444            loc: ploc,
3445        });
3446
3447        while self.check(TokenType::Comma) {
3448            self.advance();
3449            let name = self.consume(TokenType::Identifier)?;
3450            let ploc = self.loc_of(&name);
3451            self.consume(TokenType::Colon)?;
3452            let type_expr = self.parse_type_expr()?;
3453            params.push(Parameter {
3454                name: name.value,
3455                type_expr,
3456                loc: ploc,
3457            });
3458        }
3459        Ok(params)
3460    }
3461
3462    // ── FLOW STEP dispatch ───────────────────────────────────────
3463
3464    fn parse_flow_step(&mut self) -> Result<FlowStep, ParseError> {
3465        let tok = self.current().clone();
3466
3467        match tok.ttype {
3468            // §Fase 119.f — an epistemic block INSIDE a flow body. Its
3469            // children are hoisted to program level (see `Parser::hoisted`),
3470            // which is exactly what a top-level block already does, so the
3471            // nested spelling costs nothing downstream. The flow itself gets
3472            // no node: the block declares, it does not execute.
3473            TokenType::Know | TokenType::Believe | TokenType::Speculate
3474                if self
3475                    .tokens
3476                    .get(self.pos + 1)
3477                    .is_some_and(|t| t.ttype == TokenType::LBrace) =>
3478            {
3479                let block = self.parse_epistemic_block()?;
3480                self.hoisted.push(Declaration::Epistemic(block));
3481                self.parse_flow_step()
3482            }
3483            TokenType::Doubt
3484                if self
3485                    .tokens
3486                    .get(self.pos + 1)
3487                    .is_some_and(|t| t.ttype == TokenType::LBrace) =>
3488            {
3489                let block = self.parse_epistemic_block()?;
3490                self.hoisted.push(Declaration::Epistemic(block));
3491                self.parse_flow_step()
3492            }
3493            TokenType::Step => self.parse_step().map(FlowStep::Step),
3494            TokenType::If => self.parse_if().map(FlowStep::If),
3495            TokenType::For => self.parse_for_in().map(FlowStep::ForIn),
3496            TokenType::Let => self.parse_let().map(FlowStep::Let),
3497            TokenType::Return => self.parse_return().map(FlowStep::Return),
3498            TokenType::Break => self.parse_break().map(FlowStep::Break),
3499            TokenType::Continue => self.parse_continue().map(FlowStep::Continue),
3500            TokenType::Lambda => self.parse_lambda_data_apply().map(FlowStep::LambdaDataApply),
3501
3502            // ── Tier 2 flow steps (typed AST) ─────────────────────
3503            TokenType::Probe => self.parse_flow_step_simple("probe").map(|l| FlowStep::Probe(ProbeStep { target: l.1, fields: Vec::new(), loc: l.0 })),
3504            // §Fase 119.f.8 — ONE implementation for both positions (the D119.4
3505            // doctrine). `reason <target>` and `reason { given ask depth }` are
3506            // the same node; the second is what the README publishes.
3507            TokenType::Reason => self.parse_reason_step().map(FlowStep::Reason),
3508            TokenType::Validate => self.parse_flow_step_simple("validate").map(|l| FlowStep::Validate(ValidateStep { target: l.1, rule: String::new(), loc: l.0 })),
3509            TokenType::Refine => self.parse_flow_step_simple("refine").map(|l| FlowStep::Refine(RefineStep { target: l.1, strategy: String::new(), loc: l.0 })),
3510            TokenType::Weave => self.parse_weave_step(),
3511            TokenType::Use => self.parse_use_step(),
3512            TokenType::Remember => self.parse_remember_step(),
3513            TokenType::Recall => self.parse_recall_step(),
3514            TokenType::Par => self.parse_par_block().map(FlowStep::Par),
3515            TokenType::Hibernate => self.parse_hibernate_step(),
3516            TokenType::Deliberate => self.parse_block_step("deliberate").map(|l| FlowStep::Deliberate(DeliberateBlock { loc: l })),
3517            TokenType::Consensus => self.parse_block_step("consensus").map(|l| FlowStep::Consensus(ConsensusBlock { loc: l })),
3518            TokenType::Forge => self.parse_forge_step().map(FlowStep::Forge),
3519            TokenType::Focus => self.parse_focus_step(),
3520            TokenType::Grad => self.parse_grad_step(),
3521            TokenType::Associate => self.parse_associate_step(),
3522            TokenType::Aggregate => self.parse_aggregate_step(),
3523            TokenType::Explore => self.parse_explore_step(),
3524            TokenType::Ingest => self.parse_ingest_step(),
3525            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 })),
3526            // §Fase 111.e — `stream` parses its BODY. It used to go through
3527            // `parse_block_step`, whose entire job is `skip_braced_block()` —
3528            // the block's contents were thrown away at parse time, which is why
3529            // `run_stream` had nothing to run and "completed" with an empty
3530            // string while the README sold "Algebraic Effects and Free Monads".
3531            TokenType::Stream => self.parse_stream_block().map(FlowStep::Stream),
3532            // ── §Fase 120 — algebraic effects ────────────────────
3533            //
3534            // All five constructs parse at flow level. `resume` / `abort` /
3535            // `forward` are legal only inside a handler CLAUSE — that scope law
3536            // is enforced by the type-checker (§120.d), not here, because the
3537            // parser does not know whether an enclosing `handle` exists when it
3538            // is re-entered through `parse_flow_step` from a clause body.
3539            TokenType::Handle => self.parse_handle_block().map(FlowStep::Handle),
3540            TokenType::Perform => self.parse_perform_step().map(FlowStep::Perform),
3541            TokenType::Resume => {
3542                let tok = self.consume(TokenType::Resume)?;
3543                let value_expr = self.parse_discharge_value()?;
3544                Ok(FlowStep::Resume(ResumeStep {
3545                    value_expr,
3546                    loc: self.loc_of(&tok),
3547                }))
3548            }
3549            TokenType::Abort => {
3550                let tok = self.consume(TokenType::Abort)?;
3551                let value_expr = self.parse_discharge_value()?;
3552                Ok(FlowStep::Abort(AbortStep {
3553                    value_expr,
3554                    loc: self.loc_of(&tok),
3555                }))
3556            }
3557            TokenType::Forward => self.parse_forward_step().map(FlowStep::Forward),
3558            TokenType::Navigate => self.parse_navigate_step(),
3559            TokenType::Drill => self.parse_drill_step(),
3560            TokenType::Trail => self.parse_flow_step_simple("trail").map(|l| FlowStep::Trail(TrailStep { navigate_ref: l.1, loc: l.0 })),
3561            TokenType::Corroborate => self.parse_corroborate_step(),
3562            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 })),
3563            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 })),
3564            // §Fase 111.f — `compute <Name> on a, b -> out`. The ARGUMENTS used to
3565            // be `Vec::new()` — hardcoded empty at the parse site — so even if
3566            // the runtime had wanted to compute something, it had nothing to
3567            // compute it FROM.
3568            TokenType::Compute => self.parse_compute_apply().map(FlowStep::ComputeApply),
3569            TokenType::Listen => self.parse_listen_step(),
3570            TokenType::Daemon => self.parse_flow_step_simple("daemon").map(|l| FlowStep::DaemonStep(DaemonStepNode { daemon_ref: l.1, loc: l.0 })),
3571            // §λ-L-E Fase 13 — Mobile typed channels (paper §3.1, §3.2, §4.3)
3572            TokenType::Emit => self.parse_emit_step(),
3573            // §Fase 92.b — `mint <Credential> as <binding>` (ephemeral credential).
3574            TokenType::Mint => self.parse_mint_step(),
3575            // §Fase 94.b — `rotate <SecretsStore> [where "…"] with <Tool> as
3576            // <binding>` (mediated secret renewal).
3577            TokenType::Rotate => self.parse_rotate_step(),
3578            TokenType::Publish => self.parse_publish_step(),
3579            TokenType::Discover => self.parse_discover_step(),
3580            TokenType::Persist => self.parse_persist_step(),
3581            TokenType::Retrieve => self.parse_retrieve_step(),
3582            TokenType::Mutate => self.parse_mutate_step(),
3583            TokenType::Purge => self.parse_store_where_step().map(|(loc, store_name, where_expr)| FlowStep::Purge(PurgeStep { store_name, where_expr, loc })),
3584            TokenType::Transact => self.parse_block_step("transact").map(|l| FlowStep::Transact(TransactBlock { loc: l })),
3585            // §Fase 88.a — the `warden` adversarial-analysis block.
3586            TokenType::Warden => self.parse_warden().map(FlowStep::Warden),
3587            // §Fase 51.a — the `quant` cognitive block (Hilbert-space projection).
3588            TokenType::Quant => self.parse_quant().map(FlowStep::Quant),
3589            // §Fase 51.d.2 — the `yield` measurement point.
3590            TokenType::Yield => self.parse_yield().map(FlowStep::Yield),
3591            // §Fase 52.c — `run <Flow>(args)` as a flow-step: invoke a declared
3592            // flow from inside a body (a `daemon` listen handler, Q3). Reuses
3593            // the top-level run parser.
3594            TokenType::Run => self.parse_run().map(FlowStep::Run),
3595
3596            _ => {
3597                // §Fase 28.e — append "Did you mean X?" hint when the
3598                // unknown token looks like a typo'd flow-body keyword
3599                // (e.g. `stepp` / `reasn` / `validte`). D3, D11.
3600                let hint = crate::smart_suggest::suggest_for(
3601                    &tok.value,
3602                    crate::smart_suggest::FLOW_BODY_KEYWORD_NAMES,
3603                );
3604                let base = format!(
3605                    "Unexpected token in flow body: '{}' — expected step, if, for, let, return, ...",
3606                    tok.value
3607                );
3608                let message = if hint.is_empty() {
3609                    base
3610                } else {
3611                    format!("{base}. {hint}")
3612                };
3613                Err(ParseError {
3614                    message,
3615                    line: tok.line,
3616                    column: tok.column,
3617                    ..Default::default()
3618                })
3619            }
3620        }
3621    }
3622
3623    // ── STEP ─────────────────────────────────────────────────────
3624
3625    fn parse_step(&mut self) -> Result<StepNode, ParseError> {
3626        let tok = self.consume(TokenType::Step)?;
3627        let loc = self.loc_of(&tok);
3628        let name = self.consume(TokenType::Identifier)?.value;
3629
3630        let mut persona_ref = String::new();
3631        if self.check(TokenType::Use) {
3632            self.advance();
3633            persona_ref = self.consume_any_ident_or_kw()?.value;
3634        }
3635
3636        self.consume(TokenType::LBrace)?;
3637
3638        let mut node = StepNode {
3639            name,
3640            persona_ref,
3641            given: String::new(),
3642            ask: String::new(),
3643            output_type: String::new(),
3644            confidence_floor: None,
3645            navigate_ref: String::new(),
3646            apply_ref: String::new(),
3647            requires_context: None,
3648            now_tz: None,
3649            guards: Vec::new(),
3650            pix_ops: Vec::new(),
3651            stream: None,
3652            performs: Vec::new(),
3653            loc,
3654        };
3655
3656        self.parse_step_body_into(&mut node)?;
3657        self.consume(TokenType::RBrace)?;
3658        Ok(node)
3659    }
3660
3661    /// §Fase 119.n — the step-body field/statement loop, extracted from
3662    /// [`Self::parse_step`] so a `stream<T>` handler arm can reuse it VERBATIM.
3663    ///
3664    /// The caller has already consumed the opening `{` and owns the closing `}`.
3665    ///
3666    /// Extracting it is what keeps `on_chunk: { … }` honest. The published arm
3667    /// body is a STEP body — `probe chunk for […]` followed by
3668    /// `output: QuoteSnapshot` — and `output:` has no flow-level position, so
3669    /// parsing the arm as a flow body would have rejected the README's own
3670    /// example. Re-implementing the loop instead would fork the grammar: every
3671    /// future step-body statement would have to be added twice, and the second
3672    /// copy is the one that rots.
3673    fn parse_step_body_into(&mut self, node: &mut StepNode) -> Result<(), ParseError> {
3674        while !self.check(TokenType::RBrace) {
3675            let inner = self.current().clone();
3676
3677            match inner.ttype {
3678                TokenType::Given => {
3679                    self.advance();
3680                    self.consume(TokenType::Colon)?;
3681                    node.given = self.parse_expression_string()?;
3682                }
3683                TokenType::Ask => {
3684                    self.advance();
3685                    self.consume(TokenType::Colon)?;
3686                    node.ask = self.consume(TokenType::StringLit)?.value;
3687                }
3688                TokenType::Output => {
3689                    // Mirror of Python `_parse_step` `case "output":`
3690                    // which uses `_parse_output_type_string` — accepts
3691                    // the FULL generic-aware shape `Stream<T>`,
3692                    // `Stream<T>?`, `Identifier?`, NOT just the bare
3693                    // head identifier. Pre-fix the step parser dropped
3694                    // `<T>` and downstream `flow_has_stream_output`'s
3695                    // `starts_with("Stream<") && ends_with('>')` then
3696                    // returned false → `implicit_transport == "json"`
3697                    // → dynamic routes served JSON instead of SSE.
3698                    self.advance();
3699                    self.consume(TokenType::Colon)?;
3700                    node.output_type = self.parse_output_type_string()?;
3701                }
3702                // §Fase 119.f — `navigate` in a step body is TWO forms, told
3703                // apart by the token after the keyword:
3704                //   `navigate: <Ref>`        the field (pre-§119.f)
3705                //   `navigate <Ref> query: …` the STATEMENT README publishes
3706                // The second is an elevation: it binds `as:` before the step
3707                // generates, so the step's `ask:` can interpolate it.
3708                TokenType::Navigate
3709                    if self
3710                        .tokens
3711                        .get(self.pos + 1)
3712                        .is_some_and(|t| t.ttype != TokenType::Colon) =>
3713                {
3714                    let op = self.parse_navigate_step()?;
3715                    node.pix_ops.push(op);
3716                }
3717                TokenType::Drill => {
3718                    let op = self.parse_drill_step()?;
3719                    node.pix_ops.push(op);
3720                }
3721                TokenType::Trail => {
3722                    let op = self
3723                        .parse_flow_step_simple("trail")
3724                        .map(|l| FlowStep::Trail(TrailStep { navigate_ref: l.1, loc: l.0 }))?;
3725                    node.pix_ops.push(op);
3726                }
3727                // §Fase 119.f — `validate <binding> against: <Schema>`, the
3728                // form README's pix family publishes inside a step. The
3729                // flow-level `validate <target>` already exists; this adds the
3730                // step position plus the `against:` clause the docs write.
3731                TokenType::Validate => {
3732                    let tok = self.current().clone();
3733                    self.advance();
3734                    // §Fase 119.f.10 — SUBJECT: `validate Assess.output against: X`.
3735                    let target = self.parse_subject()?;
3736                    let mut rule = String::new();
3737                    if self.current().value == "against" {
3738                        self.advance();
3739                        self.consume(TokenType::Colon)?;
3740                        rule = self.consume_any_ident_or_kw()?.value.clone();
3741                    }
3742                    node.pix_ops.push(FlowStep::Validate(ValidateStep {
3743                        target,
3744                        rule,
3745                        loc: Loc { line: tok.line, column: tok.column },
3746                    }));
3747                }
3748                TokenType::Navigate => {
3749                    self.advance();
3750                    self.consume(TokenType::Colon)?;
3751                    node.navigate_ref = self.parse_dotted_identifier()?;
3752                }
3753                TokenType::Identifier if inner.value == "confidence_floor" => {
3754                    self.advance();
3755                    self.consume(TokenType::Colon)?;
3756                    node.confidence_floor = Some(self.consume_number()?);
3757                }
3758                TokenType::Identifier if inner.value == "apply" => {
3759                    self.advance();
3760                    self.consume(TokenType::Colon)?;
3761                    node.apply_ref = self.consume_any_ident_or_kw()?.value;
3762                }
3763                // §Fase 68.b — `requires_context: <tokens>`: the step's declared
3764                // model-capability requirement (the context window the cognition
3765                // needs). A bare positive integer literal; the §68.c resolver maps
3766                // it to a concrete model. Range/ceiling is the type-checker's job
3767                // (§68.b positive-int + §68.f catalog ceiling) — the parser only
3768                // requires an integer token here (a float / non-number is a parse
3769                // error, surfaced at the exact column).
3770                TokenType::Identifier if inner.value == "requires_context" => {
3771                    self.advance();
3772                    self.consume(TokenType::Colon)?;
3773                    let num = self.current().clone();
3774                    let bad = |tok: &crate::tokens::Token| ParseError {
3775                        message: format!(
3776                            "`requires_context:` must be a positive integer token count \
3777                             (got '{}')",
3778                            tok.value
3779                        ),
3780                        line: tok.line,
3781                        column: tok.column,
3782                        ..Default::default()
3783                    };
3784                    if num.ttype != TokenType::Integer {
3785                        return Err(bad(&num));
3786                    }
3787                    let value = num.value.parse::<u32>().map_err(|_| bad(&num))?;
3788                    self.advance();
3789                    node.requires_context = Some(value);
3790                }
3791                // §Fase 91.a — `now: "<IANA-tz>"`: the step's declared cognitive
3792                // timezone. A string literal; the format law (IANA shape) is the
3793                // type-checker's job (`axon-T892`) — the parser only requires a
3794                // string token here, surfaced at the exact column.
3795                TokenType::Identifier if inner.value == "now" => {
3796                    self.advance();
3797                    self.consume(TokenType::Colon)?;
3798                    let tz = self.current().clone();
3799                    if tz.ttype != TokenType::StringLit {
3800                        return Err(ParseError {
3801                            message: format!(
3802                                "`now:` must be an IANA timezone string literal like \
3803                                 \"America/Bogota\" or \"UTC\" (got '{}')",
3804                                tz.value
3805                            ),
3806                            line: tz.line,
3807                            column: tz.column,
3808                            ..Default::default()
3809                        });
3810                    }
3811                    self.advance();
3812                    node.now_tz = Some(tz.value);
3813                }
3814                // §Fase 54.a — a `use` nested inside a `step { }` body used
3815                // to be skipped structurally (grouped with the sub-constructs
3816                // below), silently degrading the tool dispatch to an
3817                // unconstrained LLM step with NO diagnostic. That fallthrough
3818                // drops the AST node before the type-checker can see it, so the
3819                // resource the tool would provision is never linearly accounted
3820                // for (use_tool soundness). Reject it here, at the parser —
3821                // the only place that still sees the token — and redirect to
3822                // the canonical forms.
3823                TokenType::Use => {
3824                    let tool = self
3825                        .tokens
3826                        .get(self.pos + 1)
3827                        .map(|t| t.value.as_str())
3828                        .filter(|v| !v.is_empty())
3829                        .unwrap_or("<Tool>");
3830                    return Err(ParseError {
3831                        message: format!(
3832                            "`use` is not valid inside a `step {{ }}` body — the tool dispatch \
3833                             would be silently dropped. To invoke a tool, either write the \
3834                             flow-level step `use {tool} on <arg>` (outside this block), or bind \
3835                             it inside this step with `apply: {tool}`. To attach a persona, put \
3836                             it in the step header: `step <name> use <Persona> {{ … }}`."
3837                        ),
3838                        line: inner.line,
3839                        column: inner.column,
3840                        ..Default::default()
3841                    });
3842                }
3843                // §Fase 119 (D119.4) — `mandate X on Y`, `shield X on Y -> b`,
3844                // `ots X on Y` as STEP-BODY statements. README §XV has always
3845                // written the application here — next to the `output:` it
3846                // constrains — and the parser accepted the same form only at
3847                // flow level, which is why README blocks 40–42 never compiled.
3848                // The published position is also the better semantics: a
3849                // mandate inside a step is scoped to THIS step's generation;
3850                // the flow-level form governs a bare statement whose subject
3851                // must be inferred. One concept, two positions, same AST shape
3852                // as the flow-level `*ApplyStep` family.
3853                TokenType::Mandate => {
3854                    let g = self.parse_step_guard("mandate")?;
3855                    node.guards.push(g);
3856                }
3857                TokenType::Shield => {
3858                    let g = self.parse_step_guard("shield")?;
3859                    node.guards.push(g);
3860                }
3861                TokenType::Ots => {
3862                    let g = self.parse_step_guard("ots")?;
3863                    node.guards.push(g);
3864                }
3865                // §Fase 119.c — `lambda RawQuote on ticker -> verified_quote`
3866                // inside a step body: README blocks 46-47's exact shape, the
3867                // D119.4 statement position extended to the fourth member of
3868                // the apply family. Semantically it is an ELEVATION, not a
3869                // guard: dispatch runs it BEFORE the step's generation, so the
3870                // elevated binding is in scope for the prompt.
3871                TokenType::Lambda => {
3872                    let g = self.parse_step_guard("lambda")?;
3873                    node.guards.push(g);
3874                }
3875                // §Fase 119.f — `probe <target> for [a, b, c]` as a STATEMENT.
3876                //
3877                // `probe` used to fall into `skip_flow_step_structural` below,
3878                // which DISCARDED it — the §111 silent-drop shape, in the step
3879                // parser. The extraction list had nowhere to live even at flow
3880                // level. Both are fixed here: the statement is kept, and its
3881                // `for [...]` list reaches the AST.
3882                TokenType::Probe
3883                    if self
3884                        .tokens
3885                        .get(self.pos + 1)
3886                        .is_some_and(|t| t.ttype != TokenType::Colon) =>
3887                {
3888                    let tok = self.current().clone();
3889                    self.advance();
3890                    // §Fase 119.f.10 — SUBJECT: README §psyche writes
3891                    // `probe student.recent_interactions for [...]`.
3892                    let target = self.parse_subject()?;
3893                    let mut fields = Vec::new();
3894                    if self.check(TokenType::For) {
3895                        self.advance();
3896                        self.consume(TokenType::LBracket)?;
3897                        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
3898                            fields.push(self.consume_any_ident_or_kw()?.value.clone());
3899                            if self.check(TokenType::Comma) {
3900                                self.advance();
3901                            }
3902                        }
3903                        self.consume(TokenType::RBracket)?;
3904                    }
3905                    node.pix_ops.push(FlowStep::Probe(ProbeStep {
3906                        target,
3907                        fields,
3908                        loc: Loc { line: tok.line, column: tok.column },
3909                    }));
3910                }
3911                // §Fase 119.f — `use_tool <name> [with k: v, …]` as a STATEMENT.
3912                // §54.a made `use` inside a step body a hard error pointing at
3913                // the canonical forms; `use_tool` is the OTHER spelling README
3914                // publishes, and it names the tool explicitly, so there is no
3915                // ambiguity to protect against — the dispatch is not dropped,
3916                // it is recorded.
3917                TokenType::Identifier if inner.value == "use_tool" => {
3918                    let tok = self.current().clone();
3919                    self.advance();
3920                    let tool_name = self.consume_any_ident_or_kw()?.value.clone();
3921                    let args = if self.current().value == "with" {
3922                        self.advance();
3923                        let mut named: Vec<(String, String, String)> = Vec::new();
3924                        loop {
3925                            let k = self.consume_any_ident_or_kw()?.value.clone();
3926                            self.consume(TokenType::Colon)?;
3927                            // `value_kind` mirrors §60's classification: a
3928                            // string literal is a literal, anything else is a
3929                            // binding reference the runtime must look up.
3930                            let kind = if self.check(TokenType::StringLit) {
3931                                "literal"
3932                            } else {
3933                                "reference"
3934                            };
3935                            let v = self.parse_expression_string()?;
3936                            named.push((k, v, kind.to_string()));
3937                            if self.check(TokenType::Comma) {
3938                                self.advance();
3939                            } else {
3940                                break;
3941                            }
3942                        }
3943                        UseArgs::Named(named)
3944                    } else if self.current().value == "on" {
3945                        self.advance();
3946                        UseArgs::LegacyPositional(
3947                            self.consume_any_ident_or_kw()?.value.clone(),
3948                        )
3949                    } else {
3950                        UseArgs::LegacyPositional(String::new())
3951                    };
3952                    node.pix_ops.push(FlowStep::UseTool(UseToolStep {
3953                        tool_name,
3954                        args,
3955                        loc: Loc { line: tok.line, column: tok.column },
3956                    }));
3957                }
3958                // §Fase 119.f — `par { … }` inside a step body.
3959                TokenType::Par => {
3960                    let block = self.parse_par_block()?;
3961                    node.pix_ops.push(FlowStep::Par(block));
3962                }
3963                // §Fase 119.f.8 — `reason { given: … ask: "…" depth: N }` as a
3964                // step-body statement. This is the README's single most-published
3965                // cognitive form (16 blocks) and it was the most expensive
3966                // resident of the silent-drop arm below: the block reached
3967                // `skip_flow_step_structural`, which discarded it, so a step
3968                // whose ONLY cognition was a `reason` lowered to an empty `ask`
3969                // and generated over nothing. The elevation position and the
3970                // flow position share `parse_reason_step` — one concept, two
3971                // positions (D119.4).
3972                // §Fase 119.f.8 — `reason` in a step body is TWO forms, told
3973                // apart by the token after the keyword, exactly as §119.f did
3974                // for `navigate`:
3975                //
3976                //   `reason: "…"`             the FIELD — a one-line deliberation
3977                //   `reason { given ask … }`  the STATEMENT README publishes
3978                //
3979                // The field form was already written across this repo's own
3980                // fixtures and it did NOTHING: `skip_flow_step_structural`
3981                // swallowed the key AND its value. Reading it as a `reason`
3982                // whose `ask:` is that value is not new semantics — it is the
3983                // block form with one field, which is what the line says.
3984                TokenType::Reason
3985                    if self
3986                        .tokens
3987                        .get(self.pos + 1)
3988                        .is_some_and(|t| t.ttype == TokenType::Colon) =>
3989                {
3990                    let tok = self.current().clone();
3991                    self.advance();
3992                    self.consume(TokenType::Colon)?;
3993                    let mut r = ReasonStep {
3994                        strategy: String::new(),
3995                        target: String::new(),
3996                        given: String::new(),
3997                        ask: String::new(),
3998                        depth: None,
3999                        loc: self.loc_of(&tok),
4000                    };
4001                    if self.check(TokenType::StringLit) {
4002                        r.ask = self.consume(TokenType::StringLit)?.value;
4003                    } else {
4004                        r.target = self.parse_dotted_identifier()?;
4005                    }
4006                    node.pix_ops.push(FlowStep::Reason(r));
4007                }
4008                TokenType::Reason => {
4009                    let r = self.parse_reason_step()?;
4010                    node.pix_ops.push(FlowStep::Reason(r));
4011                }
4012                // §Fase 119.f.9 — `weave [a, b] format: T include: […]` as a
4013                // step-body statement: the shape fourteen README blocks close
4014                // with. It was the worst resident of the silent-drop arm below,
4015                // because it did not merely lose the node — the skipper stops
4016                // at the first `output` KEYWORD it meets, so
4017                // `weave [A.output, B.output]` left the parser mid-list and the
4018                // step then failed with `Expected Colon` pointing at the comma.
4019                // A dropped construct AND a mislocated error.
4020                TokenType::Weave => {
4021                    let w = self.parse_weave_step()?;
4022                    node.pix_ops.push(w);
4023                }
4024                // §Fase 119.m.3 — `<Agent>(arg, …)` as a step-body statement:
4025                // the form every agent example in the README uses, and the one
4026                // that makes §119.m.1's executor reachable from source.
4027                //
4028                // Told apart from the field arms above by the `(` — those all
4029                // match on a specific field NAME, so a call can never shadow
4030                // one. The name is a NAME (never dotted: an agent declaration
4031                // has no path), the arguments are §119.f.10 SUBJECTS, because
4032                // README writes `TrendAnalyzer(Gather.output)`.
4033                TokenType::Identifier
4034                    if self
4035                        .tokens
4036                        .get(self.pos + 1)
4037                        .is_some_and(|t| t.ttype == TokenType::LParen) =>
4038                {
4039                    let tok = self.current().clone();
4040                    let agent_name = self.consume_any_ident_or_kw()?.value.clone();
4041                    self.consume(TokenType::LParen)?;
4042                    let mut arguments = Vec::new();
4043                    while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
4044                        arguments.push(self.parse_subject()?);
4045                        if self.check(TokenType::Comma) {
4046                            self.advance();
4047                        }
4048                    }
4049                    self.consume(TokenType::RParen)?;
4050                    node.pix_ops.push(FlowStep::AgentCall(AgentCallStep {
4051                        agent_name,
4052                        arguments,
4053                        loc: self.loc_of(&tok),
4054                    }));
4055                }
4056                // §Fase 119.f.11 — `retrieve from <Store> where "…"` as a
4057                // step-body statement. README §axonstore writes the store read
4058                // INSIDE the step that consumes it, which is the elevation
4059                // position: the rows must be bound before the step generates.
4060                //
4061                // Unlike the three before it, this one needed no engine work —
4062                // `FlowStep::Retrieve` and `wire_integrations::run_retrieve`
4063                // are among the most-exercised paths in the system (§35–38, the
4064                // pg integration suites). Only the position was missing.
4065                TokenType::Retrieve => {
4066                    let r = self.parse_retrieve_step()?;
4067                    node.pix_ops.push(r);
4068                }
4069                // §Fase 119.n — `stream<T> { on_chunk: … on_complete: … }` in a
4070                // step body. THE LAST RESIDENT of the silent-drop arm leaves
4071                // here: `probe` left in §119.f, `reason` in §119.f.8, `weave` in
4072                // §119.f.9, `retrieve` in §119.f.11.
4073                //
4074                // What it cost, measured on README block 15 before this landed:
4075                // the whole block — a `probe`, a `validate`, and BOTH `output:`
4076                // declarations — went to `skip_flow_step_structural`, so
4077                // `step Stream` reached the dispatcher with `pix_ops=0`,
4078                // `ask=""`, `output=""`. An entirely EMPTY step, that `axon
4079                // check` passed with 0 errors, and whose `Stream.output` the
4080                // next step then reasoned over. The block had left the §117
4081                // ledger on the strength of compiling.
4082                //
4083                // NOT a `pix_ops` push — see `StepNode::stream`. The other ten
4084                // statements are elevations that run BEFORE generation; a stream
4085                // handler runs DURING it, and this step's output IS the stream.
4086                TokenType::Stream => {
4087                    let sb = self.parse_stream_block()?;
4088                    if node.stream.is_some() {
4089                        return Err(ParseError {
4090                            message:
4091                                "step declares two `stream` blocks; a step has one output stream, \
4092                                 and composing two has no defined meaning (which one is the \
4093                                 step's output?). Refused rather than silently keeping the last."
4094                                    .to_string(),
4095                            line: inner.line,
4096                            column: inner.column,
4097                            ..Default::default()
4098                        });
4099                    }
4100                    node.stream = Some(Box::new(sb));
4101                }
4102                // §Fase 120 — `perform Op(args)` in a step body, the position
4103                // `fase_23` §3.1 publishes:
4104                //
4105                //     step generate {
4106                //         given: prompt
4107                //         perform Emit(response.token)
4108                //         perform Done()
4109                //     }
4110                //
4111                // NOT a `pix_ops` push, and this is the §119.n lesson applied a
4112                // second time. Every `pix_ops` statement is an ELEVATION that
4113                // runs BEFORE the step generates. The performed ARGUMENT here is
4114                // the step's own output, so running it as an elevation would
4115                // hand the handler an unresolved symbol and put a NAME on the
4116                // wire where the adopter expected a token — a defect that shows
4117                // up as garbage output, never as an error.
4118                TokenType::Perform => {
4119                    let p = self.parse_perform_step()?;
4120                    node.performs.push(p);
4121                }
4122                // Sub-construct (probe, non-statement form) → skip structurally.
4123                // The REAL `probe … for […]` statement is taken by the guarded
4124                // arm above; this catches only the bare legacy shape.
4125                TokenType::Probe => {
4126                    self.skip_flow_step_structural()?;
4127                }
4128                _ => {
4129                    return Err(ParseError {
4130                        message: format!(
4131                            "Unexpected token in step body: '{}' — expected given, ask, \
4132                             probe, reason, weave, stream, perform, output, confidence_floor, \
4133                             navigate, apply, requires_context, now",
4134                            inner.value
4135                        ),
4136                        line: inner.line,
4137                        column: inner.column,
4138                                            ..Default::default()
4139                    });
4140                }
4141            }
4142        }
4143        Ok(())
4144    }
4145
4146    /// Skip a flow-level sub-construct structurally (consume keyword + args + optional block).
4147    fn skip_flow_step_structural(&mut self) -> Result<(), ParseError> {
4148        // Consume the keyword
4149        self.advance();
4150        // Consume tokens until we hit a { or a closing }, or a known flow step keyword
4151        while !self.check(TokenType::LBrace)
4152            && !self.check(TokenType::RBrace)
4153            && !self.check(TokenType::Eof)
4154        {
4155            // Check if we hit a new step-level keyword (means this was a one-liner)
4156            let tt = &self.current().ttype;
4157            if matches!(
4158                tt,
4159                TokenType::Step
4160                    | TokenType::Given
4161                    | TokenType::Ask
4162                    | TokenType::Output
4163                    | TokenType::Navigate
4164                    | TokenType::Use
4165                    | TokenType::Probe
4166                    | TokenType::Reason
4167                    | TokenType::Weave
4168                    | TokenType::Stream
4169                    | TokenType::If
4170                    | TokenType::For
4171                    | TokenType::Let
4172                    | TokenType::Return
4173            ) {
4174                return Ok(());
4175            }
4176            self.advance();
4177        }
4178        // If block, skip it
4179        if self.check(TokenType::LBrace) {
4180            self.skip_braced_block()?;
4181        }
4182        Ok(())
4183    }
4184
4185    // ── INTENT ───────────────────────────────────────────────────
4186
4187    fn parse_intent(&mut self) -> Result<IntentNode, ParseError> {
4188        let tok = self.consume(TokenType::Intent)?;
4189        let loc = self.loc_of(&tok);
4190        let name = self.consume(TokenType::Identifier)?.value;
4191        self.consume(TokenType::LBrace)?;
4192
4193        let mut node = IntentNode {
4194            name,
4195            given: String::new(),
4196            ask: String::new(),
4197            output_type: None,
4198            confidence_floor: None,
4199            loc,
4200            leading_trivia: Vec::new(),
4201            trailing_trivia: Vec::new(),
4202        };
4203
4204        while !self.check(TokenType::RBrace) {
4205            let field_name = self.current().value.clone();
4206            self.advance();
4207            self.consume(TokenType::Colon)?;
4208
4209            match field_name.as_str() {
4210                "given" => node.given = self.consume(TokenType::Identifier)?.value,
4211                "ask" => node.ask = self.consume(TokenType::StringLit)?.value,
4212                "output" => node.output_type = Some(self.parse_type_expr()?),
4213                "confidence_floor" => node.confidence_floor = Some(self.consume_number()?),
4214                _ => self.skip_value(),
4215            }
4216        }
4217        self.consume(TokenType::RBrace)?;
4218        Ok(node)
4219    }
4220
4221    // ── RUN ──────────────────────────────────────────────────────
4222
4223    fn parse_run(&mut self) -> Result<RunStatement, ParseError> {
4224        let tok = self.consume(TokenType::Run)?;
4225        let loc = self.loc_of(&tok);
4226        let flow_name = self.consume(TokenType::Identifier)?.value;
4227
4228        self.consume(TokenType::LParen)?;
4229        let mut arguments = Vec::new();
4230        if !self.check(TokenType::RParen) {
4231            arguments = self.parse_argument_list()?;
4232        }
4233        self.consume(TokenType::RParen)?;
4234
4235        let mut node = RunStatement {
4236            flow_name,
4237            arguments,
4238            persona: String::new(),
4239            context: String::new(),
4240            anchors: Vec::new(),
4241            on_failure: String::new(),
4242            on_failure_params: Vec::new(),
4243            output_to: String::new(),
4244            effort: String::new(),
4245            loc,
4246            leading_trivia: Vec::new(),
4247            trailing_trivia: Vec::new(),
4248        };
4249
4250        while self.check_run_modifier() {
4251            let mod_tok = self.current().clone();
4252            // §Fase 119.m.3 — `with <Persona>`, README's spelling of `as`.
4253            if mod_tok.value == "with" && mod_tok.ttype != TokenType::As {
4254                self.advance();
4255                node.persona = self.consume(TokenType::Identifier)?.value;
4256                continue;
4257            }
4258            match mod_tok.ttype {
4259                TokenType::As => {
4260                    self.advance();
4261                    node.persona = self.consume(TokenType::Identifier)?.value;
4262                }
4263                TokenType::Within => {
4264                    self.advance();
4265                    node.context = self.consume(TokenType::Identifier)?.value;
4266                }
4267                TokenType::ConstrainedBy => {
4268                    self.advance();
4269                    node.anchors = self.parse_bracketed_identifiers()?;
4270                }
4271                TokenType::OnFailure => {
4272                    self.advance();
4273                    self.consume(TokenType::Colon)?;
4274                    node.on_failure = self.consume_any_ident_or_kw()?.value;
4275                    // Parse optional params: (key: val, ...)
4276                    if self.check(TokenType::LParen) {
4277                        self.advance();
4278                        while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
4279                            let key = self.consume_any_ident_or_kw()?.value;
4280                            self.consume(TokenType::Colon)?;
4281                            let val = self.consume_any_ident_or_kw()?.value;
4282                            node.on_failure_params.push((key, val));
4283                            if self.check(TokenType::Comma) {
4284                                self.advance();
4285                            }
4286                        }
4287                        if self.check(TokenType::RParen) {
4288                            self.advance();
4289                        }
4290                    }
4291                }
4292                TokenType::OutputTo => {
4293                    self.advance();
4294                    self.consume(TokenType::Colon)?;
4295                    node.output_to = self.consume(TokenType::StringLit)?.value;
4296                }
4297                TokenType::Effort => {
4298                    self.advance();
4299                    self.consume(TokenType::Colon)?;
4300                    node.effort = self.consume_any_ident_or_kw()?.value;
4301                }
4302                _ => break,
4303            }
4304        }
4305
4306        Ok(node)
4307    }
4308
4309    // ── EPISTEMIC BLOCK ──────────────────────────────────────────
4310
4311    fn parse_epistemic_block(&mut self) -> Result<EpistemicBlock, ParseError> {
4312        let tok = self.current().clone();
4313        let mode = match tok.ttype {
4314            TokenType::Know => "know",
4315            TokenType::Believe => "believe",
4316            TokenType::Speculate => "speculate",
4317            TokenType::Doubt => "doubt",
4318            _ => unreachable!(),
4319        };
4320        self.advance();
4321        let loc = self.loc_of(&tok);
4322
4323        self.consume(TokenType::LBrace)?;
4324        let mut body = Vec::new();
4325        while !self.check(TokenType::RBrace) {
4326            body.push(self.parse_declaration()?);
4327        }
4328        self.consume(TokenType::RBrace)?;
4329
4330        Ok(EpistemicBlock {
4331            mode: mode.to_string(),
4332            body,
4333            loc,
4334            leading_trivia: Vec::new(),
4335            trailing_trivia: Vec::new(),
4336        })
4337    }
4338
4339    // ── IF ────────────────────────────────────────────────────────
4340
4341    // ── §Fase 70.a — the pure expression engine (Pratt parser) ───────────
4342
4343    /// Parse a pure expression (§Fase 70). Precedence-climbing: `or` < `and` <
4344    /// comparison < `+ -` < `* / %` < unary (`- not`) < atom. Total + pure; no
4345    /// side effects. Field/index access + the builtin catalog land in §70.c/d.
4346    fn parse_expr(&mut self) -> Result<Expr, ParseError> {
4347        self.parse_expr_bp(0)
4348    }
4349
4350    fn parse_expr_bp(&mut self, min_bp: u8) -> Result<Expr, ParseError> {
4351        // Prefix: unary `-` (negation) / `not` (boolean). Binds tighter than
4352        // every binary operator (bp 6).
4353        let mut lhs = match self.current().ttype {
4354            TokenType::Minus => {
4355                self.advance();
4356                Expr::Unary(UnOp::Neg, Box::new(self.parse_expr_bp(6)?))
4357            }
4358            TokenType::Not => {
4359                self.advance();
4360                Expr::Unary(UnOp::Not, Box::new(self.parse_expr_bp(6)?))
4361            }
4362            _ => self.parse_postfix()?,
4363        };
4364        // Infix: left-associative (right_bp = left_bp + 1).
4365        while let Some((op, lbp)) = Self::binop_of(self.current().ttype.clone()) {
4366            if lbp < min_bp {
4367                break;
4368            }
4369            self.advance();
4370            let rhs = self.parse_expr_bp(lbp + 1)?;
4371            lhs = Expr::Binary(op, Box::new(lhs), Box::new(rhs));
4372        }
4373        Ok(lhs)
4374    }
4375
4376    /// Map a token to `(BinOp, left binding power)`, or `None` if it is not an
4377    /// infix operator (which stops the climb — e.g. at `->` or `{`).
4378    fn binop_of(t: TokenType) -> Option<(BinOp, u8)> {
4379        Some(match t {
4380            TokenType::Or => (BinOp::Or, 1),
4381            TokenType::And => (BinOp::And, 2),
4382            TokenType::Eq => (BinOp::Eq, 3),
4383            TokenType::Neq => (BinOp::Ne, 3),
4384            TokenType::Lt => (BinOp::Lt, 3),
4385            TokenType::Lte => (BinOp::Le, 3),
4386            TokenType::Gt => (BinOp::Gt, 3),
4387            TokenType::Gte => (BinOp::Ge, 3),
4388            TokenType::Plus => (BinOp::Add, 4),
4389            TokenType::Minus => (BinOp::Sub, 4),
4390            TokenType::Star => (BinOp::Mul, 5),
4391            TokenType::Slash => (BinOp::Div, 5),
4392            TokenType::Percent => (BinOp::Mod, 5),
4393            _ => return None,
4394        })
4395    }
4396
4397    /// §Fase 70.c — parse a primary then its `.` postfix chain: a builtin call
4398    /// (`.length`, `.contains(x)`) when the name is in the closed catalog, else
4399    /// a dotted reference-path continuation (`a.b.c` → `Ref("a.b.c")`, the
4400    /// pre-§70.c behaviour). Field access on a non-reference (`(a+b).x`) is
4401    /// reserved for §70.d.
4402    fn parse_postfix(&mut self) -> Result<Expr, ParseError> {
4403        let mut expr = self.parse_expr_atom()?;
4404        loop {
4405            if self.check(TokenType::Dot) {
4406                self.advance();
4407                let name = self.consume_any_ident_or_kw()?.value;
4408                if let Some(builtin) = Builtin::from_name(&name) {
4409                    let mut args = vec![expr];
4410                    if self.check(TokenType::LParen) {
4411                        self.advance();
4412                        while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
4413                            args.push(self.parse_expr_bp(0)?);
4414                            if self.check(TokenType::Comma) {
4415                                self.advance();
4416                            } else {
4417                                break;
4418                            }
4419                        }
4420                        self.consume(TokenType::RParen)?;
4421                    }
4422                    expr = Expr::Call(builtin, args);
4423                } else {
4424                    // §Fase 70.d — a plain dotted path on a Ref extends the Ref
4425                    // (back-compat: `a.b.c` → `Ref("a.b.c")`); on any other base
4426                    // it is a structured field access (the JSONB seam).
4427                    expr = match expr {
4428                        Expr::Ref(p) => Expr::Ref(format!("{p}.{name}")),
4429                        other => Expr::Field(Box::new(other), name),
4430                    };
4431                }
4432            } else if self.check(TokenType::LBracket) {
4433                // §Fase 70.d — index access `base[index]`.
4434                self.advance();
4435                let index = self.parse_expr_bp(0)?;
4436                self.consume(TokenType::RBracket)?;
4437                expr = Expr::Index(Box::new(expr), Box::new(index));
4438            } else {
4439                break;
4440            }
4441        }
4442        Ok(expr)
4443    }
4444
4445    fn parse_expr_atom(&mut self) -> Result<Expr, ParseError> {
4446        let tok = self.current().clone();
4447        match tok.ttype {
4448            TokenType::Integer => {
4449                self.advance();
4450                let lit = tok
4451                    .value
4452                    .parse::<i64>()
4453                    .map(ExprLit::Int)
4454                    .or_else(|_| tok.value.parse::<f64>().map(ExprLit::Float))
4455                    .map_err(|_| ParseError {
4456                        message: format!("invalid integer literal '{}'", tok.value),
4457                        line: tok.line,
4458                        column: tok.column,
4459                        ..Default::default()
4460                    })?;
4461                Ok(Expr::Lit(lit))
4462            }
4463            TokenType::Float => {
4464                self.advance();
4465                let f = tok.value.parse::<f64>().map_err(|_| ParseError {
4466                    message: format!("invalid float literal '{}'", tok.value),
4467                    line: tok.line,
4468                    column: tok.column,
4469                    ..Default::default()
4470                })?;
4471                Ok(Expr::Lit(ExprLit::Float(f)))
4472            }
4473            TokenType::Bool => {
4474                self.advance();
4475                Ok(Expr::Lit(ExprLit::Bool(tok.value == "true")))
4476            }
4477            TokenType::StringLit => {
4478                self.advance();
4479                Ok(Expr::Lit(ExprLit::Str(tok.value)))
4480            }
4481            TokenType::LParen => {
4482                self.advance();
4483                let inner = self.parse_expr_bp(0)?;
4484                self.consume(TokenType::RParen)?;
4485                Ok(inner)
4486            }
4487            _ => {
4488                // Reference: a single identifier (or keyword used as a name).
4489                // The `.` chain (dotted path / builtin call) is handled by the
4490                // postfix layer (§70.c `parse_postfix`).
4491                Ok(Expr::Ref(self.consume_any_ident_or_kw()?.value))
4492            }
4493        }
4494    }
4495
4496    /// §Fase 70.a — render a literal to its legacy surface string (for the
4497    /// back-compat `(condition, op, value)` triple). Only used when an
4498    /// expression fits the legacy shape; numeric round-tripping is exact for
4499    /// ints and faithful-enough for floats (the legacy runtime re-parses it).
4500    fn expr_lit_surface(lit: &ExprLit) -> String {
4501        match lit {
4502            ExprLit::Int(i) => i.to_string(),
4503            ExprLit::Float(f) => f.to_string(),
4504            ExprLit::Bool(b) => b.to_string(),
4505            ExprLit::Str(s) => s.clone(),
4506        }
4507    }
4508
4509    fn expr_leaf_surface(expr: &Expr) -> Option<String> {
4510        match expr {
4511            Expr::Ref(p) => Some(p.clone()),
4512            Expr::Lit(l) => Some(Self::expr_lit_surface(l)),
4513            _ => None,
4514        }
4515    }
4516
4517    /// A legacy "leaf" is a bare reference (truthy check) or a
4518    /// `<ref> <cmp> <ref|literal>` triple — exactly what the pre-§70 `if`
4519    /// grammar could express.
4520    fn expr_legacy_leaf(expr: &Expr) -> Option<(String, String, String)> {
4521        match expr {
4522            Expr::Ref(p) => Some((p.clone(), String::new(), String::new())),
4523            Expr::Binary(op, l, r) => {
4524                let op_s = match op {
4525                    BinOp::Eq => "==",
4526                    BinOp::Ne => "!=",
4527                    BinOp::Lt => "<",
4528                    BinOp::Le => "<=",
4529                    BinOp::Gt => ">",
4530                    BinOp::Ge => ">=",
4531                    _ => return None,
4532                };
4533                let lhs = match &**l {
4534                    Expr::Ref(p) => p.clone(),
4535                    _ => return None,
4536                };
4537                let rhs = Self::expr_leaf_surface(r)?;
4538                Some((lhs, op_s.to_string(), rhs))
4539            }
4540            _ => None,
4541        }
4542    }
4543
4544    /// Flatten an `or`-tree of legacy leaves in left-to-right order. Returns
4545    /// `false` (and leaves `out` unusable) if any node is not a legacy leaf.
4546    fn collect_or_leaves(expr: &Expr, out: &mut Vec<(String, String, String)>) -> bool {
4547        match expr {
4548            Expr::Binary(BinOp::Or, l, r) => {
4549                Self::collect_or_leaves(l, out) && Self::collect_or_leaves(r, out)
4550            }
4551            _ => match Self::expr_legacy_leaf(expr) {
4552                Some(t) => {
4553                    out.push(t);
4554                    true
4555                }
4556                None => false,
4557            },
4558        }
4559    }
4560
4561    /// §Fase 70.a — if the parsed condition fits the legacy
4562    /// `(condition, op, value)` + `or`-chain shape, return the legacy fields so
4563    /// the IR + runtime stay byte-identical to pre-§70 (zero drift). `None` ⇒
4564    /// the condition uses richer forms (`and`, `not`, arithmetic, parentheses,
4565    /// nesting) and must ride the `cond` expression evaluator.
4566    #[allow(clippy::type_complexity)]
4567    fn cond_as_legacy(
4568        expr: &Expr,
4569    ) -> Option<(String, String, String, Vec<(String, String, String)>, String)> {
4570        let mut leaves = Vec::new();
4571        if !Self::collect_or_leaves(expr, &mut leaves) || leaves.is_empty() {
4572            return None;
4573        }
4574        let (c0, o0, v0) = leaves[0].clone();
4575        let rest = leaves[1..].to_vec();
4576        let conjunctor = if rest.is_empty() {
4577            String::new()
4578        } else {
4579            "or".to_string()
4580        };
4581        Some((c0, o0, v0, rest, conjunctor))
4582    }
4583
4584    fn parse_if(&mut self) -> Result<ConditionalNode, ParseError> {
4585        let tok = self.consume(TokenType::If)?;
4586        let loc = self.loc_of(&tok);
4587
4588        // §Fase 70.a — parse the condition as a pure expression, then split:
4589        // a legacy-expressible condition populates the legacy triple fields
4590        // (cond = None → byte-identical IR + eval); a richer condition rides
4591        // the `cond` expression evaluator.
4592        let expr = self.parse_expr()?;
4593        let (condition, comparison_op, comparison_value, conditions, conjunctor, cond) =
4594            match Self::cond_as_legacy(&expr) {
4595                Some((c, o, v, more, conj)) => (c, o, v, more, conj, None),
4596                None => (
4597                    String::new(),
4598                    String::new(),
4599                    String::new(),
4600                    Vec::new(),
4601                    String::new(),
4602                    Some(expr),
4603                ),
4604            };
4605
4606        let mut then_body = Vec::new();
4607        let mut else_body = Vec::new();
4608
4609        // Arrow form or block form
4610        if self.check(TokenType::Arrow) {
4611            self.advance();
4612            then_body.push(self.parse_flow_step()?);
4613        } else if self.check(TokenType::LBrace) {
4614            self.advance();
4615            while !self.check(TokenType::RBrace) {
4616                then_body.push(self.parse_flow_step()?);
4617            }
4618            self.consume(TokenType::RBrace)?;
4619        }
4620
4621        // Else branch
4622        if self.check(TokenType::Else) {
4623            self.advance();
4624            if self.check(TokenType::Arrow) {
4625                self.advance();
4626                else_body.push(self.parse_flow_step()?);
4627            } else if self.check(TokenType::LBrace) {
4628                self.advance();
4629                while !self.check(TokenType::RBrace) {
4630                    else_body.push(self.parse_flow_step()?);
4631                }
4632                self.consume(TokenType::RBrace)?;
4633            }
4634        }
4635
4636        Ok(ConditionalNode {
4637            condition,
4638            comparison_op,
4639            comparison_value,
4640            then_body,
4641            else_body,
4642            conditions,
4643            conjunctor,
4644            cond,
4645            loc,
4646        })
4647    }
4648
4649    // ── FOR IN ───────────────────────────────────────────────────
4650
4651    fn parse_for_in(&mut self) -> Result<ForInStatement, ParseError> {
4652        let tok = self.consume(TokenType::For)?;
4653        let loc = self.loc_of(&tok);
4654        let variable = self.consume(TokenType::Identifier)?.value;
4655        self.consume(TokenType::In)?;
4656        let iterable = self.parse_dotted_identifier()?;
4657
4658        self.consume(TokenType::LBrace)?;
4659        // Fase 19.e — increment loop_depth so `parse_break` /
4660        // `parse_continue` inside the body pass the scope check.
4661        // Decrement on every exit path (Ok / Err) so a parse error
4662        // mid-body does not leave the depth permanently elevated
4663        // for later top-level parsing — `?` would skip the
4664        // decrement otherwise.
4665        self.loop_depth += 1;
4666        let body_result = (|| -> Result<Vec<FlowStep>, ParseError> {
4667            let mut body = Vec::new();
4668            while !self.check(TokenType::RBrace) {
4669                body.push(self.parse_flow_step()?);
4670            }
4671            Ok(body)
4672        })();
4673        self.loop_depth -= 1;
4674        let body = body_result?;
4675        self.consume(TokenType::RBrace)?;
4676
4677        Ok(ForInStatement {
4678            variable,
4679            iterable,
4680            body,
4681            loc,
4682        })
4683    }
4684
4685    /// Fase 19.e — `break` keyword. Compile-time scope check
4686    /// (`loop_depth == 0`) rejects break outside a for-in body.
4687    fn parse_break(&mut self) -> Result<BreakStatement, ParseError> {
4688        let tok = self.consume(TokenType::Break)?;
4689        let loc = self.loc_of(&tok);
4690        if self.loop_depth == 0 {
4691            return Err(ParseError {
4692                message: "'break' outside of a for-in loop body".to_string(),
4693                line: tok.line,
4694                column: tok.column,
4695                            ..Default::default()
4696            });
4697        }
4698        Ok(BreakStatement { loc })
4699    }
4700
4701    /// Fase 19.e — `continue` keyword. Same scope check as
4702    /// `parse_break`.
4703    fn parse_continue(&mut self) -> Result<ContinueStatement, ParseError> {
4704        let tok = self.consume(TokenType::Continue)?;
4705        let loc = self.loc_of(&tok);
4706        if self.loop_depth == 0 {
4707            return Err(ParseError {
4708                message: "'continue' outside of a for-in loop body".to_string(),
4709                line: tok.line,
4710                column: tok.column,
4711                            ..Default::default()
4712            });
4713        }
4714        Ok(ContinueStatement { loc })
4715    }
4716
4717    // ── LET ──────────────────────────────────────────────────────
4718
4719    fn parse_let(&mut self) -> Result<LetStatement, ParseError> {
4720        let tok = self.consume(TokenType::Let)?;
4721        let loc = self.loc_of(&tok);
4722
4723        // Name can be an identifier or a keyword used as binding name
4724        let name = self.consume_any_ident_or_kw()?.value;
4725        // §Fase 51.c.3 — optional type annotation `let x: <TypeExpr> = …`.
4726        let type_annotation = if self.check(TokenType::Colon) {
4727            self.advance();
4728            Some(self.parse_type_expr()?)
4729        } else {
4730            None
4731        };
4732        self.consume(TokenType::Assign)?;
4733        // Fase 17.a — reset side-channel before parsing value; the
4734        // atom / expr helpers tag the kind as they descend.
4735        self.last_let_value_kind = "literal".to_string();
4736        let (value, value_ast) = self.parse_let_value_expr_with_ast()?;
4737
4738        Ok(LetStatement {
4739            identifier: name,
4740            value_expr: value,
4741            value_kind: self.last_let_value_kind.clone(),
4742            type_annotation,
4743            value_ast,
4744            loc,
4745            leading_trivia: Vec::new(),
4746            trailing_trivia: Vec::new(),
4747        })
4748    }
4749
4750    fn parse_let_value_expr(&mut self) -> Result<String, ParseError> {
4751        let atom = self.parse_let_atom()?;
4752
4753        // Arithmetic expression: collect as string
4754        if matches!(
4755            self.current().ttype,
4756            TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
4757        ) {
4758            let mut parts = vec![atom];
4759            while matches!(
4760                self.current().ttype,
4761                TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
4762            ) {
4763                parts.push(self.advance().value.clone());
4764                parts.push(self.parse_let_atom()?);
4765            }
4766            self.last_let_value_kind = "expression".to_string();
4767            return Ok(parts.join(" "));
4768        }
4769        Ok(atom)
4770    }
4771
4772    /// §Fase 70.f — parse a `let`-binding value, additionally producing a
4773    /// structured `value_ast` for the expression case. A list literal keeps the
4774    /// dedicated path; everything else is parsed through the §70 expression
4775    /// engine and classified: a bare literal / reference keeps its pre-§70
4776    /// string form (`value_ast = None`, byte-identical), while a real expression
4777    /// (`price * qty`, `recent.length`) additionally carries a `value_ast` the
4778    /// runtime evaluates for real (pre-§70.f it was treated as an opaque literal
4779    /// string). Used ONLY by `parse_let` — other value positions (list items,
4780    /// remember/stream values) keep the string-only `parse_let_value_expr`.
4781    fn parse_let_value_expr_with_ast(&mut self) -> Result<(String, Option<Expr>), ParseError> {
4782        if self.check(TokenType::LBracket) {
4783            self.last_let_value_kind = "literal".to_string();
4784            return Ok((self.parse_let_list_literal()?, None));
4785        }
4786        let expr = self.parse_expr()?;
4787        Ok(match expr {
4788            Expr::Lit(lit) => {
4789                self.last_let_value_kind = "literal".to_string();
4790                (Self::expr_lit_surface(&lit), None)
4791            }
4792            Expr::Ref(p) => {
4793                self.last_let_value_kind = "reference".to_string();
4794                (p, None)
4795            }
4796            other => {
4797                self.last_let_value_kind = "expression".to_string();
4798                (Self::render_expr(&other), Some(other))
4799            }
4800        })
4801    }
4802
4803    /// §Fase 70.f — a readable surface rendering of an expression for the
4804    /// vestigial `value_expr` string (the runtime uses `value_ast`).
4805    fn render_expr(e: &Expr) -> String {
4806        match e {
4807            Expr::Lit(l) => Self::expr_lit_surface(l),
4808            Expr::Ref(p) => p.clone(),
4809            // §Fase 119.o — surface form of a `logic { }` chain. This string is
4810            // vestigial (the runtime evaluates `value_ast`), so it renders the
4811            // shape rather than trying to reconstruct the author's layout.
4812            Expr::Let { name, value, body } => format!(
4813                "let {name} = {} in {}",
4814                Self::render_expr(value),
4815                Self::render_expr(body)
4816            ),
4817            Expr::Unary(UnOp::Neg, x) => format!("-{}", Self::render_expr(x)),
4818            Expr::Unary(UnOp::Not, x) => format!("not {}", Self::render_expr(x)),
4819            Expr::Binary(op, l, r) => {
4820                let sym = match op {
4821                    BinOp::Add => "+",
4822                    BinOp::Sub => "-",
4823                    BinOp::Mul => "*",
4824                    BinOp::Div => "/",
4825                    BinOp::Mod => "%",
4826                    BinOp::Eq => "==",
4827                    BinOp::Ne => "!=",
4828                    BinOp::Lt => "<",
4829                    BinOp::Le => "<=",
4830                    BinOp::Gt => ">",
4831                    BinOp::Ge => ">=",
4832                    BinOp::And => "and",
4833                    BinOp::Or => "or",
4834                };
4835                format!("({} {sym} {})", Self::render_expr(l), Self::render_expr(r))
4836            }
4837            Expr::Call(b, args) => {
4838                let recv = args.first().map(Self::render_expr).unwrap_or_default();
4839                let rest: Vec<String> = args.iter().skip(1).map(Self::render_expr).collect();
4840                if rest.is_empty() {
4841                    format!("{recv}.{}", b.surface())
4842                } else {
4843                    format!("{recv}.{}({})", b.surface(), rest.join(", "))
4844                }
4845            }
4846            Expr::Field(b, f) => format!("{}.{f}", Self::render_expr(b)),
4847            Expr::Index(b, i) => format!("{}[{}]", Self::render_expr(b), Self::render_expr(i)),
4848        }
4849    }
4850
4851    fn parse_let_atom(&mut self) -> Result<String, ParseError> {
4852        let tok = self.current().clone();
4853
4854        match tok.ttype {
4855            TokenType::StringLit => {
4856                self.last_let_value_kind = "literal".to_string();
4857                self.advance();
4858                Ok(tok.value)
4859            }
4860            TokenType::Integer | TokenType::Float => {
4861                self.last_let_value_kind = "literal".to_string();
4862                self.advance();
4863                Ok(tok.value)
4864            }
4865            TokenType::Bool => {
4866                self.last_let_value_kind = "literal".to_string();
4867                self.advance();
4868                Ok(tok.value)
4869            }
4870            TokenType::Identifier => {
4871                self.last_let_value_kind = "reference".to_string();
4872                self.parse_dotted_identifier()
4873            }
4874            TokenType::LBracket => {
4875                self.last_let_value_kind = "literal".to_string();
4876                self.parse_let_list_literal()
4877            }
4878            _ => {
4879                // Keywords starting a dotted path (pix.document_tree)
4880                if self.pos + 1 < self.tokens.len()
4881                    && self.tokens[self.pos + 1].ttype == TokenType::Dot
4882                {
4883                    self.last_let_value_kind = "reference".to_string();
4884                    return self.parse_dotted_identifier();
4885                }
4886                Err(ParseError {
4887                    message: format!(
4888                        "Expected value expression, found {:?}('{}')",
4889                        tok.ttype, tok.value
4890                    ),
4891                    line: tok.line,
4892                    column: tok.column,
4893                                    ..Default::default()
4894                })
4895            }
4896        }
4897    }
4898
4899    fn parse_let_list_literal(&mut self) -> Result<String, ParseError> {
4900        self.consume(TokenType::LBracket)?;
4901        let mut items = Vec::new();
4902        if !self.check(TokenType::RBracket) {
4903            items.push(self.parse_let_value_expr()?);
4904            while self.check(TokenType::Comma) {
4905                self.advance();
4906                if self.check(TokenType::RBracket) {
4907                    break; // trailing comma
4908                }
4909                items.push(self.parse_let_value_expr()?);
4910            }
4911        }
4912        self.consume(TokenType::RBracket)?;
4913        Ok(format!("[{}]", items.join(", ")))
4914    }
4915
4916    // ── RETURN ───────────────────────────────────────────────────
4917
4918    fn parse_return(&mut self) -> Result<ReturnStatement, ParseError> {
4919        let tok = self.consume(TokenType::Return)?;
4920        let loc = self.loc_of(&tok);
4921        let value = self.parse_let_value_expr()?;
4922        Ok(ReturnStatement {
4923            value_expr: value,
4924            loc,
4925        })
4926    }
4927
4928    // ── TIER 2 FLOW STEP HELPERS ────────────────────────────────────
4929
4930    /// Parse: keyword target (consumes keyword + one identifier/keyword-as-value).
4931    fn parse_flow_step_simple(&mut self, _kw: &str) -> Result<(Loc, String), ParseError> {
4932        let tok = self.current().clone();
4933        self.advance(); // consume keyword
4934        let target = if self.at_declaration_start()
4935            || self.check(TokenType::RBrace)
4936            || self.check(TokenType::Eof)
4937        {
4938            String::new()
4939        } else {
4940            self.consume_any_ident_or_kw()?.value.clone()
4941        };
4942        // Skip optional braced block
4943        if self.check(TokenType::LBrace) {
4944            self.skip_braced_block()?;
4945        }
4946        Ok((
4947            Loc {
4948                line: tok.line,
4949                column: tok.column,
4950            },
4951            target,
4952        ))
4953    }
4954
4955    /// Parse: keyword { ... } — block-level step, skip body structurally.
4956    /// §Fase 111.e — `stream { <steps> }` with a REAL body.
4957    ///
4958    /// The four block primitives (`deliberate`, `consensus`, `stream`,
4959    /// `transact`) all went through [`Self::parse_block_step`], whose entire job
4960    /// is `skip_braced_block()`. Their bodies were discarded at parse time — so
4961    /// their handlers were not no-ops through neglect, they were no-ops
4962    /// *by construction*: there was nothing in the AST to execute. §111 retracted
4963    /// `transact`; this gives `stream` its body back. `deliberate` / `consensus`
4964    /// remain body-less pending their Tier-4 disposition.
4965    fn parse_stream_block(&mut self) -> Result<StreamBlock, ParseError> {
4966        let tok = self.current().clone();
4967        let loc = self.loc_of(&tok);
4968        self.advance(); // consume `stream`
4969
4970        // §Fase 119.n — `<T>`: the CHUNK type, and the reason this is not just a
4971        // cosmetic capture. The skip loop below used to eat it: `stream<QuoteData>`
4972        // advanced straight past `<QuoteData>` looking for `{`, so the one piece of
4973        // type information the author wrote about the stream was discarded before
4974        // anything could check it.
4975        let mut chunk_type = String::new();
4976        if self.check(TokenType::Lt) {
4977            self.advance();
4978            let inner = self.parse_type_expr()?;
4979            chunk_type = if inner.generic_param.is_empty() {
4980                inner.name
4981            } else {
4982                format!("{}<{}>", inner.name, inner.generic_param)
4983            };
4984            self.consume(TokenType::Gt)?;
4985        }
4986
4987        // Tolerate the pre-111 form `stream <effect-ish tokens> { … }`: skip any
4988        // argument tokens ahead of the brace, exactly as `parse_block_step` did,
4989        // so an existing program keeps parsing. Only the BODY changes.
4990        while !self.check(TokenType::LBrace)
4991            && !self.check(TokenType::RBrace)
4992            && !self.check(TokenType::Eof)
4993            && !self.at_declaration_start()
4994        {
4995            self.advance();
4996        }
4997
4998        let mut block = StreamBlock {
4999            chunk_type,
5000            on_chunk: None,
5001            on_complete: None,
5002            on_error: None,
5003            body: Vec::new(),
5004            loc,
5005        };
5006
5007        if self.check(TokenType::LBrace) {
5008            self.advance();
5009            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5010                // §Fase 119.n — the two SPECIFIED handler arms. `fase_23`'s D8
5011                // promises `stream<τ> { on_chunk: … on_complete: … }` compiles
5012                // with "cero cambios en `.axon` source files de adopters"; before
5013                // this landed it was a hard parse error at flow level and a
5014                // silent discard in a step body.
5015                let name = self.current().value.clone();
5016                let is_arm = matches!(name.as_str(), "on_chunk" | "on_complete" | "on_error")
5017                    && self
5018                        .tokens
5019                        .get(self.pos + 1)
5020                        .is_some_and(|t| t.ttype == TokenType::Colon);
5021                if is_arm {
5022                    let arm_tok = self.current().clone();
5023                    self.advance(); // the handler name
5024                    self.advance(); // `:`
5025                    let arm = self.parse_stream_handler_arm(&name, &arm_tok)?;
5026                    let slot = match name.as_str() {
5027                        "on_chunk" => &mut block.on_chunk,
5028                        "on_complete" => &mut block.on_complete,
5029                        _ => &mut block.on_error,
5030                    };
5031                    if slot.is_some() {
5032                        return Err(ParseError {
5033                            message: format!(
5034                                "`{name}` is declared twice in this `stream` block. Two handlers \
5035                                 for one edge have no defined composition (whose output is the \
5036                                 stream's?), so the duplicate is refused rather than silently \
5037                                 overwriting the first."
5038                            ),
5039                            line: arm_tok.line,
5040                            column: arm_tok.column,
5041                            ..Default::default()
5042                        });
5043                    }
5044                    *slot = Some(arm);
5045                    continue;
5046                }
5047
5048                // A `<ident>: {` that is NOT one of the two arms is a TYPO in a
5049                // closed catalog, and the §119.h.2 discipline says to ask which
5050                // direction the silence fails in: a mis-spelled `on_chunk` would
5051                // fall through to `parse_flow_step` and be reported against the
5052                // brace, pointing the author at the wrong token entirely. Name
5053                // the key and the catalog instead.
5054                let next_two_are_block = self
5055                    .tokens
5056                    .get(self.pos + 1)
5057                    .is_some_and(|t| t.ttype == TokenType::Colon)
5058                    && self
5059                        .tokens
5060                        .get(self.pos + 2)
5061                        .is_some_and(|t| t.ttype == TokenType::LBrace);
5062                if next_two_are_block {
5063                    let bad = self.current().clone();
5064                    return Err(ParseError {
5065                        message: format!(
5066                            "unknown `stream` handler `{name}` — this block accepts only \
5067                             `on_chunk:` (run once per chunk, with the chunk bound as `chunk`), \
5068                             `on_complete:` (run once, after the source closes, with the \
5069                             accumulation bound as `complete`) and `on_error:` (run when the \
5070                             SOURCE fails, with the failure bound as `error`). An unrecognised \
5071                             handler is refused rather than skipped: a skipped handler removes \
5072                             the processing the author wrote, and silence in that direction is \
5073                             indistinguishable from a stream that had nothing to do."
5074                        ),
5075                        line: bad.line,
5076                        column: bad.column,
5077                        ..Default::default()
5078                    });
5079                }
5080
5081                // §Fase 111.e's body form, kept: `stream { <flow steps> }`.
5082                block.body.push(self.parse_flow_step()?);
5083            }
5084            self.consume(TokenType::RBrace)?;
5085        }
5086
5087        Ok(block)
5088    }
5089
5090    /// §Fase 119.n — one `on_chunk:` / `on_complete:` arm, parsed as a STEP body.
5091    ///
5092    /// The arm carries `output:` (README block 15 writes `output: QuoteSnapshot`
5093    /// in `on_chunk` and `output: VerifiedQuote` in `on_complete`), and `output:`
5094    /// is a step field with no flow-level position. Reusing
5095    /// [`Self::parse_step_body_into`] is therefore not a convenience — it is the
5096    /// only shape that accepts what the README publishes, and it means the arm
5097    /// dispatches through `run_step` like any other step.
5098    fn parse_stream_handler_arm(
5099        &mut self,
5100        name: &str,
5101        at: &Token,
5102    ) -> Result<StepNode, ParseError> {
5103        self.consume(TokenType::LBrace)?;
5104        let mut node = StepNode {
5105            name: name.to_string(),
5106            persona_ref: String::new(),
5107            given: String::new(),
5108            ask: String::new(),
5109            output_type: String::new(),
5110            confidence_floor: None,
5111            navigate_ref: String::new(),
5112            apply_ref: String::new(),
5113            requires_context: None,
5114            now_tz: None,
5115            guards: Vec::new(),
5116            pix_ops: Vec::new(),
5117            stream: None,
5118            performs: Vec::new(),
5119            loc: self.loc_of(at),
5120        };
5121        self.parse_step_body_into(&mut node)?;
5122        self.consume(TokenType::RBrace)?;
5123        Ok(node)
5124    }
5125
5126    fn parse_block_step(&mut self, _kw: &str) -> Result<Loc, ParseError> {
5127        let tok = self.current().clone();
5128        self.advance();
5129        // Skip optional arguments before brace
5130        while !self.check(TokenType::LBrace)
5131            && !self.check(TokenType::RBrace)
5132            && !self.check(TokenType::Eof)
5133            && !self.at_declaration_start()
5134        {
5135            self.advance();
5136        }
5137        if self.check(TokenType::LBrace) {
5138            self.skip_braced_block()?;
5139        }
5140        Ok(Loc {
5141            line: tok.line,
5142            column: tok.column,
5143        })
5144    }
5145
5146    /// §Fase 86 — parse `forge <Name>(seed: "<text>") -> <Type> { mode:,
5147    /// novelty:, depth:, branches:, constraints: }`. Real field capture
5148    /// (replacing the pre-§86 discard-everything stub). Strict closed-catalog:
5149    /// an unknown field is a hard parse error; all cross-field laws (Boden mode
5150    /// catalog, novelty range, depth/branches ≥ 1, `constraints:` → `anchor`)
5151    /// are §86.c type-checker territory.
5152    fn parse_forge_step(&mut self) -> Result<ForgeBlock, ParseError> {
5153        let tok = self.consume(TokenType::Forge)?;
5154        let name = self.consume(TokenType::Identifier)?.value;
5155        let mut node = ForgeBlock {
5156            name,
5157            novelty: 0.5,
5158            depth: 1,
5159            branches: 1,
5160            loc: Loc { line: tok.line, column: tok.column },
5161            ..Default::default()
5162        };
5163        // `(seed: "...")`
5164        self.consume(TokenType::LParen)?;
5165        let arg = self.consume_any_ident_or_kw()?.value;
5166        self.consume(TokenType::Colon)?;
5167        if arg != "seed" {
5168            return Err(self.error(&format!(
5169                "forge '{}' expects `seed:` as its argument, found `{arg}`",
5170                node.name
5171            )));
5172        }
5173        node.seed = self.consume(TokenType::StringLit)?.value;
5174        self.consume(TokenType::RParen)?;
5175        // `-> <Type>`
5176        self.consume(TokenType::Arrow)?;
5177        node.output_type = self.consume_any_ident_or_kw()?.value;
5178        // `{ fields }`
5179        self.consume(TokenType::LBrace)?;
5180        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5181            let field = self.consume_any_ident_or_kw()?.value;
5182            self.consume(TokenType::Colon)?;
5183            match field.as_str() {
5184                "mode" => node.mode = self.consume_any_ident_or_kw()?.value,
5185                "novelty" => node.novelty = self.consume_number()?,
5186                "depth" => {
5187                    node.depth = self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0)
5188                }
5189                "branches" => {
5190                    node.branches =
5191                        self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0)
5192                }
5193                "constraints" => node.constraints_ref = self.consume_any_ident_or_kw()?.value,
5194                other => {
5195                    return Err(self.error(&format!("unknown forge field `{other}`")))
5196                }
5197            }
5198            if self.check(TokenType::Comma) {
5199                self.consume(TokenType::Comma)?;
5200            }
5201        }
5202        self.consume(TokenType::RBrace)?;
5203        Ok(node)
5204    }
5205
5206    /// §Fase 65 — Parse `par { stmt1  stmt2  … }` into CONCURRENT branches.
5207    /// Each top-level flow statement inside the block is one branch (a
5208    /// single-statement body); they execute concurrently at runtime
5209    /// (`flow_dispatcher::parallel::run_branches_concurrently`). Before §65 the
5210    /// `par` body was skipped (`parse_block_step`), so the branches were lost
5211    /// and the handler ran as a stub. Multi-statement branches (grouping
5212    /// several steps into one sequential branch) are a future grammar
5213    /// extension; today the natural `par { step A  step B }` fans A and B out.
5214    fn parse_par_block(&mut self) -> Result<ParBlock, ParseError> {
5215        let tok = self.current().clone();
5216        self.advance(); // consume `par`
5217        self.consume(TokenType::LBrace)?;
5218        let mut branches: Vec<Vec<FlowStep>> = Vec::new();
5219        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5220            branches.push(vec![self.parse_flow_step()?]);
5221        }
5222        self.consume(TokenType::RBrace)?;
5223        Ok(ParBlock {
5224            branches,
5225            loc: Loc {
5226                line: tok.line,
5227                column: tok.column,
5228            },
5229        })
5230    }
5231
5232    /// §Fase 51.a — Parse the `quant` cognitive block surface.
5233    ///
5234    /// Grammar (the attribute header is OPTIONAL):
5235    /// ```text
5236    /// quant { <flow steps> }
5237    /// quant(encoding: amplitude, observable: M, qubits: 10,
5238    ///       depth: 4, bandwidth: 0.5, reupload: 3, backend: quant_sim) { <flow steps> }
5239    /// ```
5240    /// The bare form (the paper's example) leaves every attribute defaulted
5241    /// (`encoding = amplitude`, `effect = quant_sim`). The body is parsed into
5242    /// real nested `FlowStep`s — like `par` branches — so §51.b's Continuous
5243    /// Type Invariant scans actual AST rather than skipped tokens.
5244    /// §Fase 88.a — parse `warden(<target>) within <Scope> { <body> }`. The
5245    /// `within <Scope>` clause is MANDATORY at the grammar level (fail-closed by
5246    /// construction: a scopeless warden cannot be written); §88.c checks the
5247    /// scope RESOLVES + the target is in its allowlist.
5248    fn parse_warden(&mut self) -> Result<WardenBlock, ParseError> {
5249        let tok = self.consume(TokenType::Warden)?;
5250        // `(<target>)` — the resource under analysis.
5251        self.consume(TokenType::LParen)?;
5252        let target = self.consume_any_ident_or_kw()?.value;
5253        self.consume(TokenType::RParen)?;
5254        // `within <Scope>` — MANDATORY. Omitting it is a hard parse error.
5255        self.consume(TokenType::Within)?;
5256        let scope_ref = self.consume(TokenType::Identifier)?.value;
5257        let mut block = WardenBlock {
5258            target,
5259            scope_ref,
5260            body: Vec::new(),
5261            loc: Loc {
5262                line: tok.line,
5263                column: tok.column,
5264            },
5265        };
5266        // Body: real nested flow steps (like `quant`/`par`).
5267        self.consume(TokenType::LBrace)?;
5268        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5269            block.body.push(self.parse_flow_step()?);
5270        }
5271        self.consume(TokenType::RBrace)?;
5272        Ok(block)
5273    }
5274
5275    /// §Fase 88.a — parse `scope <Name> { targets: [ … ], depth: <ident>,
5276    /// approver: [requires] "<cap>" }`. Flat key:value block (the `cache` shape).
5277    /// Catalog + non-empty validation is §88.c. Unknown fields are a hard error
5278    /// (D83.7): a scope governs an offensive-capable analysis.
5279    fn parse_scope(&mut self) -> Result<ScopeDefinition, ParseError> {
5280        let tok = self.consume(TokenType::Scope)?;
5281        let name = self.consume(TokenType::Identifier)?.value;
5282        let mut node = ScopeDefinition {
5283            name,
5284            loc: Loc {
5285                line: tok.line,
5286                column: tok.column,
5287            },
5288            ..Default::default()
5289        };
5290        self.consume(TokenType::LBrace)?;
5291        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5292            let key = self.consume_any_ident_or_kw()?.value;
5293            self.consume(TokenType::Colon)?;
5294            match key.as_str() {
5295                "targets" => {
5296                    self.consume(TokenType::LBracket)?;
5297                    while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
5298                        let t = if self.check(TokenType::StringLit) {
5299                            self.consume(TokenType::StringLit)?.value
5300                        } else {
5301                            self.consume_any_ident_or_kw()?.value
5302                        };
5303                        node.targets.push(t);
5304                        if self.check(TokenType::Comma) {
5305                            self.advance();
5306                        }
5307                    }
5308                    self.consume(TokenType::RBracket)?;
5309                }
5310                "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
5311                "approver" => {
5312                    // Optional `requires` sugar before the capability string.
5313                    if self.current().value == "requires" {
5314                        self.advance();
5315                    }
5316                    node.approver = self.consume(TokenType::StringLit)?.value;
5317                }
5318                other => {
5319                    return Err(self.error(&format!(
5320                        "unknown scope field `{other}` in scope `{}` — expected \
5321                         `targets` / `depth` / `approver`",
5322                        node.name
5323                    )))
5324                }
5325            }
5326            if self.check(TokenType::Comma) {
5327                self.consume(TokenType::Comma)?;
5328            }
5329        }
5330        self.consume(TokenType::RBrace)?;
5331        Ok(node)
5332    }
5333
5334    fn parse_quant(&mut self) -> Result<QuantBlock, ParseError> {
5335        let tok = self.current().clone();
5336        self.advance(); // consume `quant`
5337
5338        let mut block = QuantBlock {
5339            encoding: None,
5340            observable: None,
5341            qubits: None,
5342            depth: None,
5343            bandwidth: None,
5344            reupload: None,
5345            // D1/D9 default backend: the CPU simulator effect. `qpu_native` is
5346            // opt-in via `backend: qpu_native`.
5347            effect: "quant_sim".to_string(),
5348            body: Vec::new(),
5349            loc: Loc {
5350                line: tok.line,
5351                column: tok.column,
5352            },
5353        };
5354
5355        // ── Optional attribute header: `(key: value, …)` ──
5356        if self.check(TokenType::LParen) {
5357            self.advance();
5358            while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
5359                let key = self.consume_any_ident_or_kw()?.value;
5360                self.consume(TokenType::Colon)?;
5361                match key.as_str() {
5362                    "encoding" => {
5363                        block.encoding = Some(self.consume_any_ident_or_kw()?.value)
5364                    }
5365                    "observable" => {
5366                        block.observable = Some(self.parse_dotted_identifier()?)
5367                    }
5368                    "qubits" => block.qubits = Some(self.consume_number()? as i64),
5369                    "depth" => block.depth = Some(self.consume_number()? as i64),
5370                    "bandwidth" => block.bandwidth = Some(self.consume_number()?),
5371                    // §Fase 69.c — data re-uploading layers.
5372                    "reupload" => block.reupload = Some(self.consume_number()? as i64),
5373                    // `backend:` selects the algebraic-effect tag (D1/D9).
5374                    "backend" => block.effect = self.consume_any_ident_or_kw()?.value,
5375                    other => {
5376                        return Err(ParseError {
5377                            message: format!(
5378                                "Unknown `quant` attribute `{other}` — expected one of \
5379                                 encoding, observable, qubits, depth, bandwidth, reupload, backend"
5380                            ),
5381                            line: self.current().line,
5382                            column: self.current().column,
5383                            ..Default::default()
5384                        });
5385                    }
5386                }
5387                // Optional comma between attributes (order-free, trailing-comma ok).
5388                if self.check(TokenType::Comma) {
5389                    self.advance();
5390                }
5391            }
5392            self.consume(TokenType::RParen)?;
5393        }
5394
5395        // ── Body: real nested flow steps (like `par`) ──
5396        self.consume(TokenType::LBrace)?;
5397        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5398            block.body.push(self.parse_flow_step()?);
5399        }
5400        self.consume(TokenType::RBrace)?;
5401
5402        Ok(block)
5403    }
5404
5405    /// §Fase 51.d.2 — Parse the `yield <expr>` measurement point. Reuses the
5406    /// `let`-value expression grammar (reference / literal / arithmetic) so the
5407    /// yielded value's tokenization intent is preserved in `value_kind`.
5408    fn parse_yield(&mut self) -> Result<YieldStatement, ParseError> {
5409        let tok = self.consume(TokenType::Yield)?;
5410        let loc = self.loc_of(&tok);
5411        self.last_let_value_kind = "literal".to_string();
5412        let value_expr = self.parse_let_value_expr()?;
5413        Ok(YieldStatement {
5414            value_expr,
5415            value_kind: self.last_let_value_kind.clone(),
5416            loc,
5417        })
5418    }
5419
5420    /// Parse: keyword Name on target -> output_type (apply pattern).
5421    /// §Fase 111.f — `compute <Name> on <a>, <b>, … -> <out>`.
5422    ///
5423    /// Positional arguments, bound to the compute's declared parameters in order.
5424    /// The generic [`Self::parse_apply_step`] captured a single `on <target>` and
5425    /// then the call site threw even that away (`arguments: Vec::new()`).
5426    fn parse_compute_apply(&mut self) -> Result<ComputeApplyStep, ParseError> {
5427        let tok = self.current().clone();
5428        let loc = self.loc_of(&tok);
5429        self.advance(); // consume `compute`
5430        let compute_name = self.consume_any_ident_or_kw()?.value.clone();
5431
5432        let mut arguments = Vec::new();
5433        if self.current().value == "on" {
5434            self.advance();
5435            loop {
5436                // §Fase 119.f.10 — SUBJECT position. README writes
5437                // `compute EligibilityScore on Profile.tenure, Profile.spend,
5438                // Profile.incidents -> score`; the bare-identifier read stopped
5439                // at the first dot, which is why every published `compute`
5440                // application failed on its own argument list.
5441                arguments.push(self.parse_subject()?);
5442                if self.check(TokenType::Comma) {
5443                    self.advance();
5444                } else {
5445                    break;
5446                }
5447            }
5448        }
5449
5450        let mut output_name = String::new();
5451        if self.check(TokenType::Arrow) {
5452            self.advance();
5453            output_name = self.consume_any_ident_or_kw()?.value.clone();
5454        }
5455
5456        Ok(ComputeApplyStep {
5457            compute_name,
5458            arguments,
5459            output_name,
5460            loc,
5461        })
5462    }
5463
5464    fn parse_apply_step(&mut self, _kw: &str) -> Result<(Loc, String, String, String), ParseError> {
5465        let tok = self.current().clone();
5466        self.advance(); // consume keyword
5467        let name = self.consume_any_ident_or_kw()?.value.clone();
5468        let mut target = String::new();
5469        let mut output_type = String::new();
5470        // "on" target
5471        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
5472            let next = self.current().clone();
5473            if next.value == "on" {
5474                self.advance();
5475                // §Fase 119.f.10 — SUBJECT position (the name before `on` is a
5476                // NAME and stays bare).
5477                target = self.parse_subject()?;
5478            }
5479        }
5480        // -> output_type
5481        if self.check(TokenType::Arrow) {
5482            self.advance();
5483            output_type = self.consume_any_ident_or_kw()?.value.clone();
5484        }
5485        // Skip optional braced block
5486        if self.check(TokenType::LBrace) {
5487            self.skip_braced_block()?;
5488        }
5489        Ok((
5490            Loc {
5491                line: tok.line,
5492                column: tok.column,
5493            },
5494            name,
5495            target,
5496            output_type,
5497        ))
5498    }
5499
5500    /// §Fase 119 (D119.4) — `<kind> <Name> [on <target>] [-> <binding>]` inside
5501    /// a `step { }` body.
5502    ///
5503    /// Differences from the flow-level `parse_apply_step`, both deliberate:
5504    ///
5505    /// - The target may be a CALL EXPRESSION, captured verbatim: README block
5506    ///   42 writes `mandate LegalPrecision on ContractDrafter(terms)`. The
5507    ///   flow-level form never needed this; the published step-level form does.
5508    /// - No trailing braced block is skipped. A guard is one statement; a
5509    ///   silently-skipped block after it would be the §119.b.1 defect again.
5510    fn parse_step_guard(&mut self, kind: &str) -> Result<StepGuardNode, ParseError> {
5511        let tok = self.current().clone();
5512        self.advance(); // consume the keyword
5513        let name = self.consume_any_ident_or_kw()?.value.clone();
5514        let mut target = String::new();
5515        let mut binding = String::new();
5516        if self.current().value == "on" {
5517            self.advance();
5518            // §Fase 119.f.10 — SUBJECT position. `shield S on vital_event -> safe`
5519            // already worked; `shield S on Charge.output -> x` did not.
5520            target = self.parse_subject()?;
5521            // `ContractDrafter(terms)` — capture the balanced argument list
5522            // verbatim into the target string.
5523            if self.check(TokenType::LParen) {
5524                let mut depth = 0usize;
5525                loop {
5526                    let t = self.current().clone();
5527                    match t.ttype {
5528                        TokenType::LParen => depth += 1,
5529                        TokenType::RParen => depth -= 1,
5530                        TokenType::Eof => {
5531                            return Err(ParseError {
5532                                message: format!(
5533                                    "unterminated argument list in `{kind} {name} on {target}(…`"
5534                                ),
5535                                line: t.line,
5536                                column: t.column,
5537                                ..Default::default()
5538                            })
5539                        }
5540                        _ => {}
5541                    }
5542                    target.push_str(&t.value);
5543                    self.advance();
5544                    if depth == 0 {
5545                        break;
5546                    }
5547                }
5548            }
5549        }
5550        if self.check(TokenType::Arrow) {
5551            self.advance();
5552            binding = self.consume_any_ident_or_kw()?.value.clone();
5553        }
5554        Ok(StepGuardNode {
5555            kind: kind.to_string(),
5556            name,
5557            target,
5558            binding,
5559            loc: Loc {
5560                line: tok.line,
5561                column: tok.column,
5562            },
5563        })
5564    }
5565
5566    /// §Fase 119.f.8 — `reason [<target>] [{ given: … ask: "…" depth: N }]`.
5567    ///
5568    /// Replaces the `parse_flow_step_simple("reason")` call whose entire
5569    /// treatment of the block was `skip_braced_block()`. Sixteen README blocks
5570    /// write the braced form and every one of them lowered to an empty prompt.
5571    ///
5572    /// The field set is CLOSED. An unrecognised key is an ERROR that names the
5573    /// key and lists what is accepted — the §119.h.2 discipline: a skipped
5574    /// field in a deliberation removes the deliberation (a promptless `reason`
5575    /// is silent, not loud), so the silent direction is the dangerous one.
5576    fn parse_reason_step(&mut self) -> Result<ReasonStep, ParseError> {
5577        let tok = self.current().clone();
5578        let loc = self.loc_of(&tok);
5579        self.advance(); // consume `reason`
5580
5581        // The pre-§119.f.8 positional form: `reason <target>`. Absent when the
5582        // block follows immediately, which is how the README always writes it.
5583        //
5584        // The `Colon` lookahead matters: a bare `reason` on its own line inside
5585        // a `step { }` body is followed by the step's NEXT FIELD, and without
5586        // this guard the target would swallow that field's key (`output`) and
5587        // the step would then fail on a stray `:` — an error pointing two
5588        // tokens past the actual problem. `skip_flow_step_structural` used to
5589        // absorb this shape silently; a wrong diagnostic is not an improvement
5590        // on a silent drop.
5591        let next_is_field_key = self
5592            .tokens
5593            .get(self.pos + 1)
5594            .is_some_and(|t| t.ttype == TokenType::Colon);
5595        let target = if self.check(TokenType::LBrace)
5596            || self.at_declaration_start()
5597            || self.check(TokenType::RBrace)
5598            || self.check(TokenType::Eof)
5599            || next_is_field_key
5600        {
5601            String::new()
5602        } else {
5603            self.parse_dotted_identifier()?
5604        };
5605
5606        let mut node = ReasonStep {
5607            strategy: String::new(),
5608            target,
5609            given: String::new(),
5610            ask: String::new(),
5611            depth: None,
5612            loc,
5613        };
5614
5615        if !self.check(TokenType::LBrace) {
5616            return Ok(node);
5617        }
5618        self.advance();
5619        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5620            let key = self.current().clone();
5621            self.advance();
5622            self.consume(TokenType::Colon)?;
5623            match key.value.as_str() {
5624                // `given: A.output`, `given: A.output, sessions`,
5625                // `given: [baseline.topology, current.topology]` — all three
5626                // published shapes, normalised to one comma-joined string (the
5627                // same carrier `StepNode.given` already uses).
5628                "given" => {
5629                    let mut parts = vec![self.parse_expression_string()?];
5630                    while self.check(TokenType::Comma) {
5631                        self.advance();
5632                        parts.push(self.parse_expression_string()?);
5633                    }
5634                    node.given = parts.join(", ");
5635                }
5636                "ask" => node.ask = self.consume(TokenType::StringLit)?.value,
5637                "depth" => {
5638                    let n = self.current().clone();
5639                    if n.ttype != TokenType::Integer {
5640                        return Err(ParseError {
5641                            message: format!(
5642                                "`depth:` in a `reason` block is a deliberation depth — a \
5643                                 positive integer (got '{}')",
5644                                n.value
5645                            ),
5646                            line: n.line,
5647                            column: n.column,
5648                            ..Default::default()
5649                        });
5650                    }
5651                    self.advance();
5652                    node.depth = n.value.parse::<u32>().ok();
5653                }
5654                // `chain_of_thought: enabled` is the README's spelling of a
5655                // named strategy; `strategy: <name>` is the general form. Both
5656                // land in the same field because dispatch reads one posture.
5657                "chain_of_thought" => {
5658                    let v = self.consume_any_ident_or_kw()?.value;
5659                    if v == "enabled" {
5660                        node.strategy = "chain_of_thought".to_string();
5661                    }
5662                }
5663                "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value,
5664                // `target:` is the SUBJECT — the same field the positional
5665                // `reason <target>` form fills, spelled as a key. The parity
5666                // corpus writes it (`reason about_policy { target: "…" }`) and
5667                // the block was discarded whole, so the key has never meant
5668                // anything. Giving it BOTH ways is refused rather than resolved
5669                // by fiat: two spellings of one field with different values
5670                // have no defined winner, and picking one silently is how a
5671                // program comes to mean something its author did not write.
5672                "target" => {
5673                    let v = if self.check(TokenType::StringLit) {
5674                        self.consume(TokenType::StringLit)?.value
5675                    } else {
5676                        self.parse_dotted_identifier()?
5677                    };
5678                    if !node.target.is_empty() {
5679                        return Err(ParseError {
5680                            message: format!(
5681                                "`reason {} {{ target: … }}` declares the subject twice — \
5682                                 once positionally as `{}` and once as `target: {}`. They \
5683                                 are the same field. Write one of them.",
5684                                node.target, node.target, v
5685                            ),
5686                            line: key.line,
5687                            column: key.column,
5688                            ..Default::default()
5689                        });
5690                    }
5691                    node.target = v;
5692                }
5693                other => {
5694                    return Err(ParseError {
5695                        message: format!(
5696                            "unknown field '{other}' in a `reason` block. Accepted: given, \
5697                             ask, depth, strategy, chain_of_thought, target. A field this \
5698                             block does not recognise is REFUSED rather than skipped — a \
5699                             `reason` that silently loses its `ask:` deliberates over \
5700                             nothing, and that failure is quiet."
5701                        ),
5702                        line: key.line,
5703                        column: key.column,
5704                        ..Default::default()
5705                    })
5706                }
5707            }
5708        }
5709        self.consume(TokenType::RBrace)?;
5710        Ok(node)
5711    }
5712
5713    /// §Fase 119.f.9 — the CLOSED braceless catalog for `weave`.
5714    ///
5715    /// `output` is deliberately ABSENT, for the reason `at_navigate_field`
5716    /// already records: in step-body position `output:` is the STEP's own
5717    /// field, and a shared name makes the terminator ambiguous. This is not
5718    /// hypothetical here — it is the exact bug the old skipper had, from the
5719    /// other side: `skip_flow_step_structural` STOPPED at `output`, mid-list,
5720    /// and the step then failed on a stray comma.
5721    fn at_weave_field(&self) -> bool {
5722        const FIELDS: &[&str] = &["format", "include", "priority", "style"];
5723        self.field_ahead(FIELDS)
5724    }
5725
5726    /// §Fase 119.f.9 — `weave [a, b] [into <T>] [format: … include: […]]`.
5727    ///
5728    /// Three published surfaces, one implementation (D119.4):
5729    ///   - the step-body statement — `weave [A.output, B.output]` followed by a
5730    ///     braceless `format:` / `include:` list (14 README blocks);
5731    ///   - the flow-body statement — `weave [A, B] into Report { format: T }`;
5732    ///   - the braced field form `weave { sources: […] … }`, which no published
5733    ///     block writes but which predates this fase and keeps working.
5734    fn parse_weave_step(&mut self) -> Result<FlowStep, ParseError> {
5735        let tok = self.current().clone();
5736        self.advance();
5737        let mut node = WeaveStep {
5738            sources: Vec::new(),
5739            target: String::new(),
5740            format_type: String::new(),
5741            priority: Vec::new(),
5742            style: String::new(),
5743            include: Vec::new(),
5744            loc: Loc {
5745                line: tok.line,
5746                column: tok.column,
5747            },
5748        };
5749        // `weave [A.output, B.output]` — the sources are REFERENCES, so they
5750        // are dotted. `parse_bracketed_dot_identifiers` is the same helper
5751        // `given:` uses; the pre-§119.f.9 braced form's `sources:` used the
5752        // non-dotted one, which is why a dotted source never had a spelling
5753        // that reached the AST.
5754        if self.check(TokenType::LBracket) {
5755            node.sources = self.parse_bracketed_dot_identifiers()?;
5756        } else if self.current().ttype == TokenType::Identifier
5757            && !self
5758                .tokens
5759                .get(self.pos + 1)
5760                .is_some_and(|t| t.ttype == TokenType::Colon)
5761        {
5762            // `weave Baz` — the bare positional subject every other statement
5763            // in the language takes (`probe X`, `reason X`, `validate X`), read
5764            // here as a one-element source list. It is the uniform rule, not a
5765            // special case, and it keeps parsing the shape that used to vanish
5766            // into `skip_flow_step_structural`.
5767            //
5768            // The Colon lookahead is the same guard `parse_reason_step` needs:
5769            // without it a bare `weave` would swallow the enclosing step's next
5770            // field KEY as its source.
5771            node.sources = vec![self.parse_dotted_identifier()?];
5772        }
5773        // `into <Target>` — the flow-level form's destination binding.
5774        if self.check(TokenType::Into) || self.current().value == "into" {
5775            self.advance();
5776            node.target = self.parse_dotted_identifier()?;
5777        }
5778        // The braceless continuation, terminated by the closed field catalog.
5779        while self.at_weave_field() {
5780            let f = self.current().value.clone();
5781            self.advance();
5782            self.consume(TokenType::Colon)?;
5783            match f.as_str() {
5784                "format" => node.format_type = self.consume_any_ident_or_kw()?.value.clone(),
5785                "include" => node.include = self.parse_bracketed_dot_identifiers()?,
5786                "priority" => node.priority = self.parse_bracketed_dot_identifiers()?,
5787                "style" => node.style = self.consume_any_ident_or_kw()?.value.clone(),
5788                // `at_weave_field` is the gate above; this arm is unreachable
5789                // unless the two catalogs drift apart.
5790                other => {
5791                    return Err(ParseError {
5792                        message: format!(
5793                            "`{other}` passed the `weave` field test but has no handler — \
5794                             the braceless catalog and its parser have drifted apart."
5795                        ),
5796                        line: tok.line,
5797                        column: tok.column,
5798                        ..Default::default()
5799                    })
5800                }
5801            }
5802        }
5803        if self.check(TokenType::LBrace) {
5804            self.advance();
5805            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5806                let f = self.current().value.clone();
5807                self.advance();
5808                if self.check(TokenType::Colon) {
5809                    self.advance();
5810                    match f.as_str() {
5811                        "sources" => node.sources = self.parse_bracketed_dot_identifiers()?,
5812                        "target" => node.target = self.consume_any_ident_or_kw()?.value.clone(),
5813                        "format" => {
5814                            node.format_type = self.consume_any_ident_or_kw()?.value.clone()
5815                        }
5816                        "priority" => node.priority = self.parse_bracketed_dot_identifiers()?,
5817                        "style" => node.style = self.consume_any_ident_or_kw()?.value.clone(),
5818                        // §Fase 119.f.9 — the braced form takes `include:` too,
5819                        // so the two spellings of one construct cannot disagree
5820                        // about which fields exist.
5821                        "include" => node.include = self.parse_bracketed_dot_identifiers()?,
5822                        _ => self.skip_value(),
5823                    }
5824                }
5825            }
5826            if self.check(TokenType::RBrace) {
5827                self.advance();
5828            }
5829        }
5830        Ok(FlowStep::Weave(node))
5831    }
5832
5833    fn parse_use_step(&mut self) -> Result<FlowStep, ParseError> {
5834        let tok = self.current().clone();
5835        self.advance();
5836        let tool_name = self.consume_any_ident_or_kw()?.value.clone();
5837        // §Fase 58.b — two mutually-exclusive `use` argument surfaces:
5838        //   * `use Tool(query = "${q}", max_results = 5)` — D2 canonical
5839        //     multi-field keyword args (§58.b `UseArgs::Named`).
5840        //   * `use Tool on "${arg}"` / `on query` — the §54.b single positional
5841        //     argument (D5 back-compat, `UseArgs::LegacyPositional`):
5842        //       - a STRING LITERAL carrying interpolation (`on "${query}"`)
5843        //         resolved at dispatch against request-bound flow params;
5844        //       - a BARE identifier / literal (`on query` / `on 42`) verbatim.
5845        //     (Unquoted `${query}` is intentionally NOT a form — interpolation
5846        //     lives inside string literals everywhere in Axon.)
5847        let args = if self.check(TokenType::LParen) {
5848            UseArgs::Named(self.parse_named_arg_list()?)
5849        } else {
5850            let mut argument = String::new();
5851            if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
5852                let next = self.current().clone();
5853                if next.value == "on" {
5854                    self.advance();
5855                    argument = self.consume_any_ident_or_kw()?.value.clone();
5856                }
5857            }
5858            UseArgs::LegacyPositional(argument)
5859        };
5860        if self.check(TokenType::LBrace) {
5861            self.skip_braced_block()?;
5862        }
5863        Ok(FlowStep::UseTool(UseToolStep {
5864            tool_name,
5865            args,
5866            loc: Loc {
5867                line: tok.line,
5868                column: tok.column,
5869            },
5870        }))
5871    }
5872
5873    /// §Fase 58.b — parse `(name = value, …)` keyword args for the canonical
5874    /// `use Tool(...)` multi-field dispatch. Values are captured as expression
5875    /// strings (StringLit / Integer / Float / Bool / dotted identifier / list)
5876    /// via the shared `parse_let_atom`, since the frontend has no structured
5877    /// `Expr`. A trailing comma is tolerated; `()` yields no args.
5878    fn parse_named_arg_list(&mut self) -> Result<Vec<(String, String, String)>, ParseError> {
5879        self.consume(TokenType::LParen)?;
5880        let mut args = Vec::new();
5881        while !self.check(TokenType::RParen) {
5882            // Accept a keyword-as-name (`filter`, `type`, `from`, …) — real
5883            // adopter schemas use such names; the following `=` disambiguates.
5884            let name = self.consume_any_ident_or_kw()?.value;
5885            self.consume(TokenType::Assign)?;
5886            let value = self.parse_let_atom()?;
5887            // §Fase 60 — `parse_let_atom` classified the value (`"literal"` vs
5888            // `"reference"`); carry it so the runtime resolves a bare
5889            // identifier / `Step.output` as a binding lookup, not a literal.
5890            let value_kind = self.last_let_value_kind.clone();
5891            args.push((name, value, value_kind));
5892            if self.check(TokenType::Comma) {
5893                self.advance();
5894            } else {
5895                break;
5896            }
5897        }
5898        self.consume(TokenType::RParen)?;
5899        Ok(args)
5900    }
5901
5902    fn parse_remember_step(&mut self) -> Result<FlowStep, ParseError> {
5903        let tok = self.current().clone();
5904        self.advance();
5905        let expr = self.consume_any_ident_or_kw()?.value.clone();
5906        let mut mem = String::new();
5907        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
5908            let next = self.current().clone();
5909            if next.value == "in" || next.ttype == TokenType::In {
5910                self.advance();
5911                mem = self.consume_any_ident_or_kw()?.value.clone();
5912            }
5913        }
5914        Ok(FlowStep::Remember(RememberStep {
5915            expression: expr,
5916            memory_target: mem,
5917            loc: Loc {
5918                line: tok.line,
5919                column: tok.column,
5920            },
5921        }))
5922    }
5923
5924    fn parse_recall_step(&mut self) -> Result<FlowStep, ParseError> {
5925        let tok = self.current().clone();
5926        self.advance();
5927        let query = if self.check(TokenType::StringLit) {
5928            self.consume(TokenType::StringLit)?.value.clone()
5929        } else {
5930            self.consume_any_ident_or_kw()?.value.clone()
5931        };
5932        let mut mem = String::new();
5933        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
5934            let next = self.current().clone();
5935            if next.value == "from" || next.ttype == TokenType::From {
5936                self.advance();
5937                mem = self.consume_any_ident_or_kw()?.value.clone();
5938            }
5939        }
5940        Ok(FlowStep::Recall(RecallStep {
5941            query,
5942            memory_source: mem,
5943            loc: Loc {
5944                line: tok.line,
5945                column: tok.column,
5946            },
5947        }))
5948    }
5949
5950    fn parse_hibernate_step(&mut self) -> Result<FlowStep, ParseError> {
5951        let tok = self.current().clone();
5952        self.advance();
5953        let mut event = String::new();
5954        let mut timeout = String::new();
5955        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
5956            // §Fase 119.d — README §III writes `hibernate until "event_name"`
5957            // (the `until` keyword + a STRING event). The parser accepted only
5958            // the bare-identifier form, so the published block never compiled.
5959            // Both forms resolve to the same field.
5960            let first = self.consume_any_ident_or_kw()?.value.clone();
5961            if first == "until" && self.check(TokenType::StringLit) {
5962                event = self.consume(TokenType::StringLit)?.value.clone();
5963            } else {
5964                event = first;
5965            }
5966        }
5967        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
5968            let next = self.current().clone();
5969            if next.ttype == TokenType::Duration {
5970                self.advance();
5971                timeout = next.value.clone();
5972            }
5973        }
5974        Ok(FlowStep::Hibernate(HibernateStep {
5975            event_name: event,
5976            timeout,
5977            loc: Loc {
5978                line: tok.line,
5979                column: tok.column,
5980            },
5981        }))
5982    }
5983
5984    /// §Fase 108.d — `focus <Dataspace> { where: "<filter>", select: [cols], as: <name> }`
5985    /// — σ_φ ∘ π_v over a declared dataspace. The `where:` string is the
5986    /// §35 data-plane filter grammar (D108.9, shared with retrieve /
5987    /// navigate). Pre-108.d the optional body was silently discarded.
5988    /// §Fase 109.a — `grad <letName> wrt <x> [as <name>]` /
5989    /// `grad <letName> wrt [a, b] as <name>`. The differentiation itself
5990    /// happens at CHECK/IR time (T931/T932 + the symbolic differentiator);
5991    /// the parser only captures the surface.
5992    fn parse_grad_step(&mut self) -> Result<FlowStep, ParseError> {
5993        let tok = self.current().clone();
5994        self.advance();
5995        let target = self.consume_any_ident_or_kw()?.value.clone();
5996        let mut wrt: Vec<String> = Vec::new();
5997        let mut output = String::new();
5998        if !self.at_declaration_start() && self.current().value == "wrt" {
5999            self.advance();
6000            if self.check(TokenType::LBracket) {
6001                wrt = self.parse_bracketed_identifiers()?;
6002            } else {
6003                wrt.push(self.consume_any_ident_or_kw()?.value.clone());
6004            }
6005        }
6006        if !self.at_declaration_start() && self.current().value == "as" {
6007            self.advance();
6008            output = self.consume_any_ident_or_kw()?.value.clone();
6009        }
6010        Ok(FlowStep::Grad(GradStep {
6011            target,
6012            wrt,
6013            output,
6014            loc: Loc {
6015                line: tok.line,
6016                column: tok.column,
6017            },
6018        }))
6019    }
6020
6021    fn parse_focus_step(&mut self) -> Result<FlowStep, ParseError> {
6022        let tok = self.current().clone();
6023        self.advance();
6024        let expression = if self.at_declaration_start()
6025            || self.check(TokenType::RBrace)
6026            || self.check(TokenType::Eof)
6027        {
6028            String::new()
6029        } else {
6030            self.consume_any_ident_or_kw()?.value.clone()
6031        };
6032        let mut where_expr = String::new();
6033        let mut select: Vec<String> = Vec::new();
6034        let mut output = String::new();
6035        if self.check(TokenType::LBrace) {
6036            self.advance();
6037            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6038                if self.check(TokenType::Comma) {
6039                    self.advance();
6040                    continue;
6041                }
6042                let f = self.current().value.clone();
6043                self.advance();
6044                if self.check(TokenType::Colon) {
6045                    self.advance();
6046                    match f.as_str() {
6047                        "where" => {
6048                            where_expr = self.consume(TokenType::StringLit)?.value.clone()
6049                        }
6050                        "select" => select = self.parse_bracketed_identifiers()?,
6051                        "as" | "alias" => {
6052                            output = self.consume_any_ident_or_kw()?.value.clone()
6053                        }
6054                        _ => self.skip_value(),
6055                    }
6056                }
6057            }
6058            if self.check(TokenType::RBrace) {
6059                self.advance();
6060            }
6061        }
6062        Ok(FlowStep::Focus(FocusStep {
6063            expression,
6064            where_expr,
6065            select,
6066            output,
6067            loc: Loc {
6068                line: tok.line,
6069                column: tok.column,
6070            },
6071        }))
6072    }
6073
6074    fn parse_associate_step(&mut self) -> Result<FlowStep, ParseError> {
6075        let tok = self.current().clone();
6076        self.advance();
6077        let left = self.consume_any_ident_or_kw()?.value.clone();
6078        let mut right = String::new();
6079        let mut using = String::new();
6080        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6081            right = self.consume_any_ident_or_kw()?.value.clone();
6082        }
6083        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6084            let next = self.current().clone();
6085            if next.value == "using" {
6086                self.advance();
6087                using = self.consume_any_ident_or_kw()?.value.clone();
6088            }
6089        }
6090        let mut output = String::new();
6091        if self.check(TokenType::LBrace) {
6092            self.advance();
6093            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6094                let f = self.current().value.clone();
6095                self.advance();
6096                if self.check(TokenType::Colon) {
6097                    self.advance();
6098                    match f.as_str() {
6099                        "as" | "alias" => output = self.consume_any_ident_or_kw()?.value.clone(),
6100                        _ => self.skip_value(),
6101                    }
6102                }
6103            }
6104            if self.check(TokenType::RBrace) {
6105                self.advance();
6106            }
6107        }
6108        Ok(FlowStep::Associate(AssociateStep {
6109            left,
6110            right,
6111            using_field: using,
6112            output,
6113            loc: Loc {
6114                line: tok.line,
6115                column: tok.column,
6116            },
6117        }))
6118    }
6119
6120    fn parse_aggregate_step(&mut self) -> Result<FlowStep, ParseError> {
6121        let tok = self.current().clone();
6122        self.advance();
6123        let target = self.consume_any_ident_or_kw()?.value.clone();
6124        let mut group_by = Vec::new();
6125        let mut alias = String::new();
6126        let mut compute: Vec<String> = Vec::new();
6127        let mut where_expr = String::new();
6128        if self.check(TokenType::LBrace) {
6129            self.advance();
6130            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6131                let f = self.current().value.clone();
6132                self.advance();
6133                if self.check(TokenType::Colon) {
6134                    self.advance();
6135                    match f.as_str() {
6136                        "group_by" => group_by = self.parse_bracketed_identifiers()?,
6137                        "alias" | "as" => alias = self.consume_any_ident_or_kw()?.value.clone(),
6138                        // §Fase 108.d — the closed aggregate catalog, kept
6139                        // RAW (`count`, `sum(score)`, …); T930 validates.
6140                        "compute" => compute = self.parse_bracketed_aggregates()?,
6141                        // §Fase 108.d — the data-plane where (D108.9).
6142                        "where" => where_expr = self.consume(TokenType::StringLit)?.value.clone(),
6143                        _ => self.skip_value(),
6144                    }
6145                }
6146            }
6147            if self.check(TokenType::RBrace) {
6148                self.advance();
6149            }
6150        }
6151        Ok(FlowStep::Aggregate(AggregateStep {
6152            target,
6153            group_by,
6154            alias,
6155            compute,
6156            where_expr,
6157            loc: Loc {
6158                line: tok.line,
6159                column: tok.column,
6160            },
6161        }))
6162    }
6163
6164    fn parse_explore_step(&mut self) -> Result<FlowStep, ParseError> {
6165        let tok = self.current().clone();
6166        self.advance();
6167        let target = self.consume_any_ident_or_kw()?.value.clone();
6168        let mut limit = None;
6169        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6170            if self.current().ttype == TokenType::Integer {
6171                limit = self.current().value.parse::<i64>().ok();
6172                self.advance();
6173            }
6174        }
6175        let mut output = String::new();
6176        if self.check(TokenType::LBrace) {
6177            self.advance();
6178            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6179                let f = self.current().value.clone();
6180                self.advance();
6181                if self.check(TokenType::Colon) {
6182                    self.advance();
6183                    match f.as_str() {
6184                        "as" | "alias" => output = self.consume_any_ident_or_kw()?.value.clone(),
6185                        _ => self.skip_value(),
6186                    }
6187                }
6188            }
6189            if self.check(TokenType::RBrace) {
6190                self.advance();
6191            }
6192        }
6193        Ok(FlowStep::ExploreStep(ExploreStepNode {
6194            target,
6195            limit,
6196            output,
6197            loc: Loc {
6198                line: tok.line,
6199                column: tok.column,
6200            },
6201        }))
6202    }
6203
6204    /// §Fase 108.d — parse `[count, sum(score), avg(x)]`: bracketed
6205    /// aggregate entries, each `ident` or `ident(ident)`, kept raw.
6206    fn parse_bracketed_aggregates(&mut self) -> Result<Vec<String>, ParseError> {
6207        let mut out = Vec::new();
6208        self.consume(TokenType::LBracket)?;
6209        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
6210            let name = self.consume_any_ident_or_kw()?.value.clone();
6211            if self.check(TokenType::LParen) {
6212                self.advance();
6213                let col = self.consume_any_ident_or_kw()?.value.clone();
6214                self.consume(TokenType::RParen)?;
6215                out.push(format!("{name}({col})"));
6216            } else {
6217                out.push(name);
6218            }
6219            if self.check(TokenType::Comma) {
6220                self.advance();
6221            }
6222        }
6223        self.consume(TokenType::RBracket)?;
6224        Ok(out)
6225    }
6226
6227    /// §Fase 108.c — the governed ingest step:
6228    ///
6229    /// ```text
6230    /// ingest <sourceRef> into <Dataspace> {
6231    ///     format: csv | json
6232    ///     limits { max_bytes: N, max_rows: N }
6233    /// }
6234    /// ```
6235    ///
6236    /// Until 108.c the body was consumed by `skip_braced_block()`. Now it
6237    /// is a closed grammar: `format:` (raw here; required + validated by
6238    /// `axon-T929`) and an optional `limits { … }` block whose bounds are
6239    /// enforced on the raw byte stream BEFORE parsing (§100). An unknown
6240    /// body entry is a parse error.
6241    fn parse_ingest_step(&mut self) -> Result<FlowStep, ParseError> {
6242        let tok = self.current().clone();
6243        self.advance();
6244        let source = self.consume_any_ident_or_kw()?.value.clone();
6245        let mut target = String::new();
6246        let mut format = String::new();
6247        let mut max_bytes: Option<u64> = None;
6248        let mut max_rows: Option<u64> = None;
6249        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6250            let next = self.current().clone();
6251            if next.value == "into" || next.ttype == TokenType::Into {
6252                self.advance();
6253                target = self.consume_any_ident_or_kw()?.value.clone();
6254            }
6255        }
6256        if self.check(TokenType::LBrace) {
6257            self.consume(TokenType::LBrace)?;
6258            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6259                // Optional separators between body entries.
6260                if self.check(TokenType::Comma) {
6261                    self.advance();
6262                    continue;
6263                }
6264                let entry = self.current().clone();
6265                match entry.value.as_str() {
6266                    "format" => {
6267                        self.advance();
6268                        self.consume(TokenType::Colon)?;
6269                        format = self.consume_any_ident_or_kw()?.value.clone();
6270                    }
6271                    "limits" => {
6272                        self.advance();
6273                        self.consume(TokenType::LBrace)?;
6274                        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6275                            let bound = self.current().clone();
6276                            self.advance();
6277                            self.consume(TokenType::Colon)?;
6278                            let num_tok = self.consume(TokenType::Integer)?.clone();
6279                            let value = num_tok.value.parse::<u64>().map_err(|_| ParseError {
6280                                message: format!(
6281                                    "ingest `limits` bound `{}` must be a non-negative \
6282                                     integer byte/row count, got `{}`.",
6283                                    bound.value, num_tok.value
6284                                ),
6285                                line: num_tok.line,
6286                                column: num_tok.column,
6287                                ..Default::default()
6288                            })?;
6289                            match bound.value.as_str() {
6290                                "max_bytes" => max_bytes = Some(value),
6291                                "max_rows" => max_rows = Some(value),
6292                                other => {
6293                                    return Err(ParseError {
6294                                        message: format!(
6295                                            "Unknown ingest limit `{other}`. The closed \
6296                                             limits grammar is `max_bytes: <N>` and \
6297                                             `max_rows: <N>` — bounds enforced on the raw \
6298                                             stream BEFORE parsing (§100).",
6299                                        ),
6300                                        line: bound.line,
6301                                        column: bound.column,
6302                                        ..Default::default()
6303                                    });
6304                                }
6305                            }
6306                            if self.check(TokenType::Comma) {
6307                                self.advance();
6308                            }
6309                        }
6310                        self.consume(TokenType::RBrace)?;
6311                    }
6312                    other => {
6313                        return Err(ParseError {
6314                            message: format!(
6315                                "Unknown entry `{other}` in ingest body. The closed \
6316                                 grammar is `format: csv|json` and \
6317                                 `limits {{ max_bytes: <N>, max_rows: <N> }}`.",
6318                            ),
6319                            line: entry.line,
6320                            column: entry.column,
6321                            ..Default::default()
6322                        });
6323                    }
6324                }
6325            }
6326            self.consume(TokenType::RBrace)?;
6327        }
6328        Ok(FlowStep::Ingest(IngestStep {
6329            source,
6330            target,
6331            format,
6332            max_bytes,
6333            max_rows,
6334            loc: Loc {
6335                line: tok.line,
6336                column: tok.column,
6337            },
6338        }))
6339    }
6340
6341    /// §Fase 119.f — is the cursor on a `navigate` field (`<name>:`)?
6342    ///
6343    /// The continuation test for the braceless field list. Closed catalog by
6344    /// construction: a name outside it ends the navigate and belongs to the
6345    /// enclosing step, which is exactly what makes the delimiter-free form
6346    /// unambiguous.
6347    fn at_navigate_field(&self) -> bool {
6348        const FIELDS: &[&str] = &[
6349            // §Fase 119.f — `output` is deliberately ABSENT from the
6350            // BRACELESS catalog even though the braced form accepts it as an
6351            // alias for `as`. In step-body position `output:` is the STEP's
6352            // own field, and a shared name would make the terminator
6353            // ambiguous — the braceless navigate would swallow the step's
6354            // output type. README writes `as:` in this position throughout;
6355            // the braced/flow-level form keeps both spellings.
6356            "corpus", "query", "trail", "as", "from", "budget", "where",
6357            "depth", "recall",
6358        ];
6359        self.field_ahead(FIELDS)
6360    }
6361
6362    /// §Fase 119.f — the same test for `drill`.
6363    fn at_drill_field(&self) -> bool {
6364        // Same reason as `at_navigate_field`: no `output` in the braceless
6365        // catalog, because that name belongs to the enclosing step.
6366        const FIELDS: &[&str] = &["subtree", "path", "query", "as"];
6367        self.field_ahead(FIELDS)
6368    }
6369
6370    /// `<one of names>` immediately followed by `:`.
6371    fn field_ahead(&self, names: &[&str]) -> bool {
6372        let cur = self.current();
6373        if !names.contains(&cur.value.as_str()) {
6374            return false;
6375        }
6376        self.tokens
6377            .get(self.pos + 1)
6378            .is_some_and(|t| t.ttype == TokenType::Colon)
6379    }
6380
6381    /// §Fase 119.f — a CONFIG KEY: `"env:DATABASE_URL"` or the bare
6382    /// `env:DATABASE_URL` README publishes.
6383    ///
6384    /// §113 made `connection:`/`endpoint:` a config KEY rather than a URL or a
6385    /// DSN — the address resolves per deployment. README writes both the
6386    /// quoted and the bare spelling; the parser took only the quoted one, so
6387    /// every published `axonstore` with an unquoted key failed on its own
6388    /// third line. One value, two spellings — the epsilon/tolerance
6389    /// resolution of §119.b.1, applied to the config surface.
6390    fn parse_config_key(&mut self) -> Result<String, ParseError> {
6391        if self.check(TokenType::StringLit) {
6392            return Ok(self.consume(TokenType::StringLit)?.value.clone());
6393        }
6394        let scheme = self.consume_any_ident_or_kw()?.value.clone();
6395        if self.check(TokenType::Colon) {
6396            self.advance();
6397            let key = self.consume_any_ident_or_kw()?.value.clone();
6398            return Ok(format!("{scheme}:{key}"));
6399        }
6400        Ok(scheme)
6401    }
6402
6403    /// §Fase 119.f — a PIX field value: a string literal OR a binding
6404    /// reference. README writes `query: question` (the flow parameter) far
6405    /// more often than a literal, and the parser accepted only the literal —
6406    /// which is why every published `navigate` failed on its own second line.
6407    fn parse_pix_value(&mut self) -> Result<String, ParseError> {
6408        if self.check(TokenType::StringLit) {
6409            return Ok(self.consume(TokenType::StringLit)?.value.clone());
6410        }
6411        Ok(self.consume_any_ident_or_kw()?.value.clone())
6412    }
6413
6414    fn parse_navigate_step(&mut self) -> Result<FlowStep, ParseError> {
6415        let tok = self.current().clone();
6416        self.advance();
6417        let pix_name = self.consume_any_ident_or_kw()?.value.clone();
6418        let mut node = NavigateStep {
6419            depth: None,
6420            pix_name,
6421            corpus_name: String::new(),
6422            query_expr: String::new(),
6423            trail_enabled: false,
6424            output_name: String::new(),
6425            seed: String::new(),
6426            budget: None,
6427            where_expr: String::new(),
6428            loc: Loc {
6429                line: tok.line,
6430                column: tok.column,
6431            },
6432        };
6433        // §Fase 119.f — the BRACELESS field form, which is what README §pix/
6434        // §corpus publishes everywhere:
6435        //
6436        //     navigate ContractIndex
6437        //         query: question
6438        //         trail: enabled
6439        //         as: relevant_sections
6440        //
6441        // Terminated by the field-name set, not by a brace: the navigate
6442        // fields are a CLOSED catalog, so "the next token is one of these and
6443        // is followed by a colon" is an unambiguous continuation test. That is
6444        // the same closed-catalog discipline the rest of the language uses,
6445        // and it is why this form needs no delimiter to be parseable.
6446        if !self.check(TokenType::LBrace) {
6447            while self.at_navigate_field() {
6448                let f = self.current().value.clone();
6449                self.advance();
6450                self.consume(TokenType::Colon)?;
6451                match f.as_str() {
6452                    "corpus" => node.corpus_name = self.consume_any_ident_or_kw()?.value.clone(),
6453                    "query" => node.query_expr = self.parse_pix_value()?,
6454                    "trail" => {
6455                        let v = self.consume_any_ident_or_kw()?.value;
6456                        node.trail_enabled = matches!(v.as_str(), "true" | "enabled" | "on");
6457                    }
6458                    "output" | "as" => {
6459                        node.output_name = self.consume_any_ident_or_kw()?.value.clone()
6460                    }
6461                    "from" => node.seed = self.consume_any_ident_or_kw()?.value.clone(),
6462                    "budget" => node.budget = self.parse_optional_int(),
6463                    "where" => node.where_expr = self.parse_pix_value()?,
6464                    "depth" => node.depth = self.parse_optional_int(),
6465                    // §Fase 119.f — `recall: episodic` selects the MDN memory
6466                    // mode README's clinical/legal examples write. The
6467                    // navigator's episodic path is §63.C's adaptive corpus
6468                    // reinforcement, keyed by the corpus declaration; the
6469                    // value is accepted and recorded on the seed so nothing
6470                    // is silently dropped, and the adaptive path already
6471                    // reads the corpus-level flag.
6472                    "recall" => {
6473                        let mode = self.consume_any_ident_or_kw()?.value.clone();
6474                        if node.seed.is_empty() {
6475                            node.seed = format!("recall:{mode}");
6476                        }
6477                    }
6478                    _ => self.skip_value(),
6479                }
6480            }
6481        }
6482        if self.check(TokenType::LBrace) {
6483            self.advance();
6484            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6485                let f = self.current().value.clone();
6486                self.advance();
6487                if self.check(TokenType::Colon) {
6488                    self.advance();
6489                    match f.as_str() {
6490                        "corpus" => {
6491                            node.corpus_name = self.consume_any_ident_or_kw()?.value.clone()
6492                        }
6493                        "query" => node.query_expr = self.parse_pix_value()?,
6494                        "trail" => {
6495                            let v = self.consume_any_ident_or_kw()?.value;
6496                            node.trail_enabled =
6497                                matches!(v.as_str(), "true" | "enabled" | "on");
6498                        }
6499                        "output" | "as" => {
6500                            node.output_name = self.consume_any_ident_or_kw()?.value.clone()
6501                        }
6502                        // §Fase 63.B — MDN corpus-graph navigation.
6503                        "from" => node.seed = self.consume_any_ident_or_kw()?.value.clone(),
6504                        "budget" => node.budget = self.parse_optional_int(),
6505                        // §Fase 66 (Q2) — column-scoped navigation: a raw filter
6506                        // expr (mirrors `retrieve … where`) pushed to the SELECT
6507                        // that sources the corpus `documents:`/`relations:` rows,
6508                        // so a `corpus from axonstore` is scoped to a sub-tenant
6509                        // COLUMN (`where: "tenant_id == '${tenant_id}'"`), not just
6510                        // the axon-tenant RLS scope. Resolved by the §37.d filter
6511                        // compiler at runtime (`${name}` → `$N` bind params).
6512                        "where" => {
6513                            node.where_expr = self.consume(TokenType::StringLit)?.value.clone()
6514                        }
6515                        _ => self.skip_value(),
6516                    }
6517                }
6518            }
6519            if self.check(TokenType::RBrace) {
6520                self.advance();
6521            }
6522        }
6523        Ok(FlowStep::Navigate(node))
6524    }
6525
6526    fn parse_drill_step(&mut self) -> Result<FlowStep, ParseError> {
6527        let tok = self.current().clone();
6528        self.advance();
6529        let pix_name = self.consume_any_ident_or_kw()?.value.clone();
6530        let mut node = DrillStep {
6531            pix_name,
6532            subtree_path: String::new(),
6533            query_expr: String::new(),
6534            output_name: String::new(),
6535            loc: Loc {
6536                line: tok.line,
6537                column: tok.column,
6538            },
6539        };
6540        // §Fase 119.f — `drill <Ref> into "<path>" query: … as: …`, the form
6541        // README publishes. `into` is a positional keyword (no colon), the
6542        // rest is the same braceless closed-catalog field list as `navigate`.
6543        if self.current().value == "into" {
6544            self.advance();
6545            // §Fase 119.f — README writes BOTH `into "Liabilities"` (a title)
6546            // and `into findings.top_region` (a dotted binding path). The
6547            // subtree path is dot-separated either way, so both spellings
6548            // land in the same field.
6549            node.subtree_path = if self.check(TokenType::StringLit) {
6550                self.consume(TokenType::StringLit)?.value.clone()
6551            } else {
6552                self.parse_dotted_identifier()?
6553            };
6554        }
6555        if !self.check(TokenType::LBrace) {
6556            while self.at_drill_field() {
6557                let f = self.current().value.clone();
6558                self.advance();
6559                self.consume(TokenType::Colon)?;
6560                match f.as_str() {
6561                    "subtree" | "path" => {
6562                        node.subtree_path = self.consume(TokenType::StringLit)?.value.clone()
6563                    }
6564                    "query" => node.query_expr = self.parse_pix_value()?,
6565                    "output" | "as" => {
6566                        node.output_name = self.consume_any_ident_or_kw()?.value.clone()
6567                    }
6568                    _ => self.skip_value(),
6569                }
6570            }
6571        }
6572        if self.check(TokenType::LBrace) {
6573            self.advance();
6574            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6575                let f = self.current().value.clone();
6576                self.advance();
6577                if self.check(TokenType::Colon) {
6578                    self.advance();
6579                    match f.as_str() {
6580                        "subtree" | "path" => {
6581                            node.subtree_path = self.consume(TokenType::StringLit)?.value.clone()
6582                        }
6583                        "query" => node.query_expr = self.parse_pix_value()?,
6584                        "output" | "as" => {
6585                            node.output_name = self.consume_any_ident_or_kw()?.value.clone()
6586                        }
6587                        _ => self.skip_value(),
6588                    }
6589                }
6590            }
6591            if self.check(TokenType::RBrace) {
6592                self.advance();
6593            }
6594        }
6595        Ok(FlowStep::Drill(node))
6596    }
6597
6598    fn parse_corroborate_step(&mut self) -> Result<FlowStep, ParseError> {
6599        let tok = self.current().clone();
6600        self.advance();
6601        let nav_ref = self.consume_any_ident_or_kw()?.value.clone();
6602        let mut output = String::new();
6603        if self.check(TokenType::Arrow) {
6604            self.advance();
6605            output = self.consume_any_ident_or_kw()?.value.clone();
6606        }
6607        Ok(FlowStep::Corroborate(CorroborateStep {
6608            navigate_ref: nav_ref,
6609            output_name: output,
6610            loc: Loc {
6611                line: tok.line,
6612                column: tok.column,
6613            },
6614        }))
6615    }
6616
6617    fn parse_listen_step(&mut self) -> Result<FlowStep, ParseError> {
6618        let tok = self.current().clone();
6619        self.advance();
6620        // §λ-L-E Fase 13 D4 — dual-mode listen:
6621        //   • String topic (legacy, deprecated since Fase 13)
6622        //   • Identifier (canonical: declared ChannelDefinition)
6623        let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
6624            (self.consume(TokenType::StringLit)?.value.clone(), false)
6625        } else {
6626            (self.consume_any_ident_or_kw()?.value.clone(), true)
6627        };
6628        let mut alias = String::new();
6629        if !self.at_declaration_start()
6630            && !self.check(TokenType::RBrace)
6631            && !self.check(TokenType::LBrace)
6632        {
6633            let next = self.current().clone();
6634            if next.value == "as" || next.ttype == TokenType::As {
6635                self.advance();
6636                alias = self.consume_any_ident_or_kw()?.value.clone();
6637            }
6638        }
6639        // §Fase 52.a — parse the handler body into real flow-steps (was
6640        // `skip_braced_block`'d, leaving the listener inert). The body runs on
6641        // each event / scheduled tick.
6642        let body = self.parse_listener_body()?;
6643        Ok(FlowStep::Listen(ListenStep {
6644            channel,
6645            channel_is_ref,
6646            event_alias: alias,
6647            body,
6648            loc: Loc {
6649                line: tok.line,
6650                column: tok.column,
6651            },
6652        }))
6653    }
6654
6655    /// §Fase 52.a — parse a `listen … { <flow steps> }` handler body. The body
6656    /// is OPTIONAL (a bodyless `listen channel` returns an empty Vec); when
6657    /// present, each statement is a real [`FlowStep`] (the same grammar as a
6658    /// flow / `quant` / `par` body), executed per trigger by the §52.c runtime.
6659    fn parse_listener_body(&mut self) -> Result<Vec<FlowStep>, ParseError> {
6660        let mut body = Vec::new();
6661        if self.check(TokenType::LBrace) {
6662            self.advance(); // consume `{`
6663            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6664                body.push(self.parse_flow_step()?);
6665            }
6666            self.consume(TokenType::RBrace)?;
6667        }
6668        Ok(body)
6669    }
6670
6671    /// §Fase 119.f.11 — `retrieve [from] <Store> [where "<expr>"] [as <alias>]`
6672    /// alongside the pre-existing braced `retrieve <Store> { where: … as: … }`.
6673    ///
6674    /// README §axonstore writes the braceless form with `from` and with `where`
6675    /// taking its argument DIRECTLY — no colon. Neither spelling parsed, so the
6676    /// only published `retrieve` failed on its own first line.
6677    fn parse_retrieve_step(&mut self) -> Result<FlowStep, ParseError> {
6678        let tok = self.current().clone();
6679        self.advance();
6680        // `from` is optional noise-with-meaning: it reads as English and the
6681        // store name carries the content either way.
6682        if self.check(TokenType::From) || self.current().value == "from" {
6683            self.advance();
6684        }
6685        let store = self.consume_any_ident_or_kw()?.value.clone();
6686        let mut where_expr = String::new();
6687        let mut alias = String::new();
6688        let mut order_by = String::new();
6689        let mut limit_expr = String::new();
6690        let mut aggregate = String::new();
6691        let mut group_by = String::new();
6692        let mut cache = String::new();
6693        // §Fase 119.f.11 — the BRACELESS clauses README publishes. Note they
6694        // take their argument with NO colon (`where "…"`, `as record`), which
6695        // is why the closed-catalog `field_ahead` test used elsewhere does not
6696        // apply: the terminator here is the clause keyword itself. Both names
6697        // are absent from the step-body field set, so a `retrieve` written
6698        // inside a step cannot swallow the step's own fields.
6699        loop {
6700            match self.current().value.as_str() {
6701                "where" if !self.check(TokenType::LBrace) => {
6702                    self.advance();
6703                    where_expr = self.consume(TokenType::StringLit)?.value.clone();
6704                }
6705                "as" => {
6706                    self.advance();
6707                    alias = self.consume_any_ident_or_kw()?.value.clone();
6708                }
6709                _ => break,
6710            }
6711        }
6712        if self.check(TokenType::LBrace) {
6713            self.advance();
6714            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6715                let f = self.current().value.clone();
6716                self.advance();
6717                if self.check(TokenType::Colon) {
6718                    self.advance();
6719                    match f.as_str() {
6720                        "where" => where_expr = self.consume(TokenType::StringLit)?.value.clone(),
6721                        "as" | "alias" => alias = self.consume_any_ident_or_kw()?.value.clone(),
6722                        // §Fase 67.b — `order_by:` is a string literal
6723                        // (`"col asc, col2 desc"`), same surface as `where:`.
6724                        "order_by" => {
6725                            order_by = self.consume(TokenType::StringLit)?.value.clone()
6726                        }
6727                        // §Fase 67.b — `limit:` is a bare integer literal
6728                        // (`limit: 100`) OR a string carrying a binding
6729                        // (`limit: "${max}"`). Captured raw; the runtime
6730                        // resolves + validates it as a `u32`.
6731                        "limit" => {
6732                            let t = self.current().clone();
6733                            match t.ttype {
6734                                TokenType::Integer | TokenType::StringLit => {
6735                                    limit_expr = t.value.clone();
6736                                    self.advance();
6737                                }
6738                                _ => self.skip_value(),
6739                            }
6740                        }
6741                        // §Fase 76.d — `aggregate:` is a string literal from
6742                        // the CLOSED catalog (`"count"`, `"sum(tokens)"`, …);
6743                        // `group_by:` is a string literal listing columns
6744                        // (`"industry, status"`). Both captured raw; the
6745                        // §38.d proof (axon-T843/T844/T845) + the runtime
6746                        // (`filter::parse_aggregate_clause`) validate.
6747                        "aggregate" => {
6748                            aggregate = self.consume(TokenType::StringLit)?.value.clone()
6749                        }
6750                        "group_by" => {
6751                            group_by = self.consume(TokenType::StringLit)?.value.clone()
6752                        }
6753                        // §Fase 85.b — `cache:` names a declared `cache`
6754                        // policy. A retrieve reads a store (never `pure`), so
6755                        // caching it always accepts staleness — the checker
6756                        // requires a finite `ttl:` on the referenced cache
6757                        // (axon-T865) and resolves the reference (axon-T864).
6758                        "cache" => cache = self.consume_any_ident_or_kw()?.value.clone(),
6759                        _ => self.skip_value(),
6760                    }
6761                }
6762            }
6763            if self.check(TokenType::RBrace) {
6764                self.advance();
6765            }
6766        }
6767        Ok(FlowStep::Retrieve(RetrieveStep {
6768            store_name: store,
6769            where_expr,
6770            alias,
6771            order_by,
6772            limit_expr,
6773            aggregate,
6774            group_by,
6775            cache,
6776            loc: Loc {
6777                line: tok.line,
6778                column: tok.column,
6779            },
6780        }))
6781    }
6782
6783    /// §Fase 35.m — Parse a `purge` step, capturing the optional
6784    /// `{ where: "<expr>" }` filter. (Fase 35.p moved `mutate` to its
6785    /// own `parse_mutate_step`, which also captures SET columns; this
6786    /// helper now serves `purge` alone — a `DELETE` has no SET clause.)
6787    ///
6788    /// Before Fase 35.m these two steps parsed via `parse_flow_step_simple`,
6789    /// which *skipped* the braced block — so a written `where:` clause
6790    /// was silently dropped and every `mutate`/`purge` ran against the
6791    /// whole store, leaving the entire Fase 35.b/c parameterized-filter
6792    /// machinery unreachable for them. This mirror of `parse_retrieve_step`
6793    /// (minus the `as:` alias — a mutate/purge binds no result) closes
6794    /// that gap. Returns `(loc, store_name, where_expr)`.
6795    fn parse_store_where_step(
6796        &mut self,
6797    ) -> Result<(Loc, String, String), ParseError> {
6798        let tok = self.current().clone();
6799        self.advance(); // consume the keyword
6800        let store = if self.at_declaration_start()
6801            || self.check(TokenType::RBrace)
6802            || self.check(TokenType::Eof)
6803        {
6804            String::new()
6805        } else {
6806            self.consume_any_ident_or_kw()?.value.clone()
6807        };
6808        let mut where_expr = String::new();
6809        if self.check(TokenType::LBrace) {
6810            self.advance();
6811            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6812                let field = self.current().value.clone();
6813                self.advance();
6814                if self.check(TokenType::Colon) {
6815                    self.advance();
6816                    match field.as_str() {
6817                        "where" => {
6818                            where_expr =
6819                                self.consume(TokenType::StringLit)?.value.clone()
6820                        }
6821                        _ => self.skip_value(),
6822                    }
6823                }
6824            }
6825            if self.check(TokenType::RBrace) {
6826                self.advance();
6827            }
6828        }
6829        Ok((
6830            Loc {
6831                line: tok.line,
6832                column: tok.column,
6833            },
6834            store,
6835            where_expr,
6836        ))
6837    }
6838
6839    /// §Fase 35.o — Parse a `persist` step, capturing the optional
6840    /// `{ col: value }` field block.
6841    ///
6842    /// Before Fase 35.o `persist` parsed via `parse_flow_step_simple`,
6843    /// which *skipped* the braced block — so a written field block was
6844    /// silently dropped and the runtime fell back to writing every
6845    /// context binding as a row, which fails against any real table
6846    /// (flows always carry more bindings than a table has columns).
6847    /// This captures the declared columns into `PersistStep.fields`;
6848    /// the runtime writes exactly those (interpolated). A `persist`
6849    /// with no block keeps the v1.30.0 user-bindings fallback — fully
6850    /// backward-compatible. Mirror of `parse_retrieve_step`, but the
6851    /// keys are arbitrary column names rather than the fixed
6852    /// `where:` / `as:` filter keys.
6853    ///
6854    /// The optional `into` connector (`persist into <store>`) is
6855    /// accepted and skipped — before Fase 35.o `into` was captured as
6856    /// the store name.
6857    fn parse_persist_step(&mut self) -> Result<FlowStep, ParseError> {
6858        let tok = self.current().clone();
6859        self.advance(); // consume `persist`
6860        // Optional `into` connector — skip it so the store name that
6861        // follows is not mistaken for the target.
6862        if self.current().value == "into" && !self.check(TokenType::LBrace) {
6863            self.advance();
6864        }
6865        let store = if self.at_declaration_start()
6866            || self.check(TokenType::LBrace)
6867            || self.check(TokenType::RBrace)
6868            || self.check(TokenType::Eof)
6869        {
6870            String::new()
6871        } else {
6872            self.consume_any_ident_or_kw()?.value.clone()
6873        };
6874        let mut fields: Vec<(String, String)> = Vec::new();
6875        if self.check(TokenType::LBrace) {
6876            self.advance();
6877            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6878                let col = self.current().value.clone();
6879                self.advance();
6880                if self.check(TokenType::Colon) {
6881                    self.advance();
6882                    let value = if self.check(TokenType::StringLit) {
6883                        self.consume(TokenType::StringLit)?.value.clone()
6884                    } else if self.check(TokenType::RBrace)
6885                        || self.check(TokenType::Eof)
6886                        || self.check(TokenType::Colon)
6887                    {
6888                        String::new()
6889                    } else {
6890                        let v = self.current().clone();
6891                        self.advance();
6892                        v.value.clone()
6893                    };
6894                    fields.push((col, value));
6895                }
6896            }
6897            if self.check(TokenType::RBrace) {
6898                self.advance();
6899            }
6900        }
6901        Ok(FlowStep::Persist(PersistStep {
6902            store_name: store,
6903            fields,
6904            loc: Loc {
6905                line: tok.line,
6906                column: tok.column,
6907            },
6908        }))
6909    }
6910
6911    /// §Fase 35.p — Parse a `mutate` step, capturing both the
6912    /// `{ where: "<expr>" }` filter AND the `{ col: value }` SET
6913    /// assignments.
6914    ///
6915    /// Before Fase 35.p `mutate` parsed via `parse_store_where_step`,
6916    /// which captured only `where:` and *skipped* every other key — so
6917    /// the runtime built the `UPDATE … SET` clause from every flow
6918    /// binding (params + step results + `let`s), which fails against
6919    /// any real table (`column "X" does not exist`). This closes the
6920    /// gap symmetrically to 35.o's `persist` block: every key other
6921    /// than `where:` is a SET column; a `mutate` with no SET column
6922    /// keeps the v1.31.0 user-bindings fallback. `where:` keeps its
6923    /// string-literal grammar (as in `retrieve` / `purge`).
6924    fn parse_mutate_step(&mut self) -> Result<FlowStep, ParseError> {
6925        let tok = self.current().clone();
6926        self.advance(); // consume `mutate`
6927        let store = if self.at_declaration_start()
6928            || self.check(TokenType::LBrace)
6929            || self.check(TokenType::RBrace)
6930            || self.check(TokenType::Eof)
6931        {
6932            String::new()
6933        } else {
6934            self.consume_any_ident_or_kw()?.value.clone()
6935        };
6936        let mut where_expr = String::new();
6937        let mut fields: Vec<(String, String)> = Vec::new();
6938        if self.check(TokenType::LBrace) {
6939            self.advance();
6940            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6941                let key = self.current().value.clone();
6942                self.advance();
6943                if self.check(TokenType::Colon) {
6944                    self.advance();
6945                    if key == "where" {
6946                        where_expr =
6947                            self.consume(TokenType::StringLit)?.value.clone();
6948                    } else {
6949                        let value = if self.check(TokenType::StringLit) {
6950                            self.consume(TokenType::StringLit)?.value.clone()
6951                        } else if self.check(TokenType::RBrace)
6952                            || self.check(TokenType::Eof)
6953                            || self.check(TokenType::Colon)
6954                        {
6955                            String::new()
6956                        } else {
6957                            let v = self.current().clone();
6958                            self.advance();
6959                            v.value.clone()
6960                        };
6961                        fields.push((key, value));
6962                    }
6963                }
6964            }
6965            if self.check(TokenType::RBrace) {
6966                self.advance();
6967            }
6968        }
6969        Ok(FlowStep::Mutate(MutateStep {
6970            store_name: store,
6971            where_expr,
6972            fields,
6973            loc: Loc {
6974                line: tok.line,
6975                column: tok.column,
6976            },
6977        }))
6978    }
6979
6980    // ── TIER 2 DECLARATIONS ────────────────────────────────────────
6981
6982    fn parse_agent(&mut self) -> Result<AgentDefinition, ParseError> {
6983        let tok = self.consume(TokenType::Agent)?;
6984        let name = self.consume(TokenType::Identifier)?.value;
6985        let mut node = AgentDefinition {
6986            name,
6987            goal: String::new(),
6988            tools: Vec::new(),
6989            memory_ref: String::new(),
6990            strategy: String::new(),
6991            on_stuck: String::new(),
6992            shield_ref: String::new(),
6993            max_iterations: None,
6994            max_tokens: None,
6995            max_time: String::new(),
6996            max_cost: None,
6997            loc: Loc {
6998                line: tok.line,
6999                column: tok.column,
7000            },
7001            leading_trivia: Vec::new(),
7002            trailing_trivia: Vec::new(),
7003        };
7004        // Skip optional parameters/return type before brace
7005        while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
7006            self.advance();
7007        }
7008        self.consume(TokenType::LBrace)?;
7009        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7010            let field = self.current().clone();
7011            let field_name = field.value.clone();
7012            self.advance();
7013            if self.check(TokenType::Colon) {
7014                self.advance();
7015                match field_name.as_str() {
7016                    "goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
7017                    "tools" => node.tools = self.parse_bracketed_identifiers()?,
7018                    "memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
7019                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
7020                    "on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
7021                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
7022                    "max_iterations" => node.max_iterations = self.parse_optional_int(),
7023                    "max_tokens" => node.max_tokens = self.parse_optional_int(),
7024                    "max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
7025                    "max_cost" => node.max_cost = self.parse_optional_float(),
7026                    _ => self.skip_value(),
7027                }
7028            } else if self.check(TokenType::LBrace) {
7029                self.skip_braced_block()?;
7030            }
7031        }
7032        self.consume(TokenType::RBrace)?;
7033        Ok(node)
7034    }
7035
7036    /// §Fase 53 — `extension Name { category: effects|scan, members: [ … ] }`.
7037    /// The parser is permissive on field/category VALUES (validated in
7038    /// §53.c by the type-checker — no-shadowing, category-membership);
7039    /// it only enforces the structural grammar here.
7040    fn parse_extension(&mut self) -> Result<ExtensionDefinition, ParseError> {
7041        let tok = self.consume(TokenType::Extension)?;
7042        let name = self.consume(TokenType::Identifier)?.value;
7043        let mut node = ExtensionDefinition {
7044            name,
7045            category: String::new(),
7046            members: Vec::new(),
7047            loc: Loc {
7048                line: tok.line,
7049                column: tok.column,
7050            },
7051            leading_trivia: Vec::new(),
7052            trailing_trivia: Vec::new(),
7053        };
7054        self.consume(TokenType::LBrace)?;
7055        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7056            let field_name = self.current().value.clone();
7057            self.advance();
7058            if self.check(TokenType::Colon) {
7059                self.advance();
7060                match field_name.as_str() {
7061                    "category" => {
7062                        node.category = self.consume_any_ident_or_kw()?.value.clone()
7063                    }
7064                    "members" => node.members = self.parse_extension_members()?,
7065                    _ => self.skip_value(),
7066                }
7067            } else if self.check(TokenType::LBrace) {
7068                self.skip_braced_block()?;
7069            }
7070        }
7071        self.consume(TokenType::RBrace)?;
7072        Ok(node)
7073    }
7074
7075    /// §Fase 53 — parse `[ "name" [ : { semantics: "…", default_confidence: 0.8 } ], … ]`.
7076    /// Each member is a string literal optionally followed by a metadata
7077    /// block. Trailing/interleaved commas are tolerated.
7078    fn parse_extension_members(&mut self) -> Result<Vec<ExtensionMember>, ParseError> {
7079        let mut members = Vec::new();
7080        self.consume(TokenType::LBracket)?;
7081        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
7082            let name_tok = self.consume(TokenType::StringLit)?;
7083            let mut member = ExtensionMember {
7084                name: name_tok.value.clone(),
7085                semantics: None,
7086                default_confidence: None,
7087                loc: Loc {
7088                    line: name_tok.line,
7089                    column: name_tok.column,
7090                },
7091            };
7092            // Optional `: { semantics: "…", default_confidence: 0.8 }`.
7093            if self.check(TokenType::Colon) {
7094                self.advance();
7095                self.consume(TokenType::LBrace)?;
7096                while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7097                    let mkey = self.current().value.clone();
7098                    self.advance();
7099                    if self.check(TokenType::Colon) {
7100                        self.advance();
7101                        match mkey.as_str() {
7102                            "semantics" => {
7103                                member.semantics =
7104                                    Some(self.consume(TokenType::StringLit)?.value.clone())
7105                            }
7106                            "default_confidence" => {
7107                                member.default_confidence = self.parse_optional_float()
7108                            }
7109                            _ => self.skip_value(),
7110                        }
7111                    }
7112                    if self.check(TokenType::Comma) {
7113                        self.advance();
7114                    }
7115                }
7116                self.consume(TokenType::RBrace)?;
7117            }
7118            members.push(member);
7119            if self.check(TokenType::Comma) {
7120                self.advance();
7121            }
7122        }
7123        self.consume(TokenType::RBracket)?;
7124        Ok(members)
7125    }
7126
7127    /// §Fase 71.a/e — `window <Name> { timezone: "…"  allow: [ {days hours} ]
7128    /// exclude: [ "YYYY-MM-DD", … ]  on_outside: skip|defer|warn }`.
7129    fn parse_window(&mut self) -> Result<WindowDefinition, ParseError> {
7130        let tok = self.consume(TokenType::Window)?;
7131        let name = self.consume(TokenType::Identifier)?.value;
7132        let mut node = WindowDefinition {
7133            name,
7134            timezone: String::new(),
7135            allow: Vec::new(),
7136            exclude: Vec::new(),
7137            on_outside: String::new(),
7138            loc: Loc {
7139                line: tok.line,
7140                column: tok.column,
7141            },
7142            leading_trivia: Vec::new(),
7143            trailing_trivia: Vec::new(),
7144        };
7145        self.consume(TokenType::LBrace)?;
7146        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7147            let field_name = self.consume_any_ident_or_kw()?.value;
7148            self.consume(TokenType::Colon)?;
7149            match field_name.as_str() {
7150                "timezone" => node.timezone = self.consume(TokenType::StringLit)?.value,
7151                "allow" => node.allow = self.parse_window_allow()?,
7152                "exclude" => node.exclude = self.parse_window_exclude()?,
7153                "on_outside" => node.on_outside = self.consume_any_ident_or_kw()?.value,
7154                _ => self.skip_value(),
7155            }
7156        }
7157        self.consume(TokenType::RBrace)?;
7158        Ok(node)
7159    }
7160
7161    /// §Fase 71.a — the `allow: [ { … }, { … } ]` span list.
7162    fn parse_window_allow(&mut self) -> Result<Vec<WindowSpan>, ParseError> {
7163        self.consume(TokenType::LBracket)?;
7164        let mut spans = Vec::new();
7165        if !self.check(TokenType::RBracket) {
7166            spans.push(self.parse_window_span()?);
7167            while self.check(TokenType::Comma) {
7168                self.advance();
7169                if self.check(TokenType::RBracket) {
7170                    break; // trailing comma
7171                }
7172                spans.push(self.parse_window_span()?);
7173            }
7174        }
7175        self.consume(TokenType::RBracket)?;
7176        Ok(spans)
7177    }
7178
7179    /// §Fase 71.e — the `exclude: [ "YYYY-MM-DD", … ]` holiday list (ISO
7180    /// date-string literals; validated for real-calendar-date-ness by the
7181    /// `axon-T826` type check). An empty list / absent field ⇒ no holidays.
7182    fn parse_window_exclude(&mut self) -> Result<Vec<String>, ParseError> {
7183        self.consume(TokenType::LBracket)?;
7184        let mut dates = Vec::new();
7185        if !self.check(TokenType::RBracket) {
7186            dates.push(self.consume(TokenType::StringLit)?.value);
7187            while self.check(TokenType::Comma) {
7188                self.advance();
7189                if self.check(TokenType::RBracket) {
7190                    break; // trailing comma
7191                }
7192                dates.push(self.consume(TokenType::StringLit)?.value);
7193            }
7194        }
7195        self.consume(TokenType::RBracket)?;
7196        Ok(dates)
7197    }
7198
7199    /// §Fase 71.a — one span `{ days: Mon..Fri  hours: 9..18 }`.
7200    fn parse_window_span(&mut self) -> Result<WindowSpan, ParseError> {
7201        let tok = self.consume(TokenType::LBrace)?;
7202        let mut span = WindowSpan {
7203            day_start: String::new(),
7204            day_end: String::new(),
7205            hour_start: 0,
7206            hour_end: 0,
7207            loc: Loc {
7208                line: tok.line,
7209                column: tok.column,
7210            },
7211        };
7212        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7213            let field = self.consume_any_ident_or_kw()?.value;
7214            self.consume(TokenType::Colon)?;
7215            match field.as_str() {
7216                "days" => {
7217                    span.day_start = self.consume_any_ident_or_kw()?.value;
7218                    self.consume(TokenType::DotDot)?;
7219                    span.day_end = self.consume_any_ident_or_kw()?.value;
7220                }
7221                "hours" => {
7222                    span.hour_start = self.consume_number()? as i64;
7223                    self.consume(TokenType::DotDot)?;
7224                    span.hour_end = self.consume_number()? as i64;
7225                }
7226                _ => self.skip_value(),
7227            }
7228            if self.check(TokenType::Comma) {
7229                self.advance();
7230            }
7231        }
7232        self.consume(TokenType::RBrace)?;
7233        Ok(span)
7234    }
7235
7236    fn parse_shield(&mut self) -> Result<ShieldDefinition, ParseError> {
7237        let tok = self.consume(TokenType::Shield)?;
7238        let name = self.consume(TokenType::Identifier)?.value;
7239        let mut node = ShieldDefinition {
7240            name,
7241            scan: Vec::new(),
7242            strategy: String::new(),
7243            on_breach: String::new(),
7244            severity: String::new(),
7245            quarantine: String::new(),
7246            max_retries: None,
7247            confidence_threshold: None,
7248            allow_tools: Vec::new(),
7249            deny_tools: Vec::new(),
7250            sandbox: None,
7251            redact: Vec::new(),
7252            log: String::new(),
7253            deflect_message: String::new(),
7254            taint: String::new(),
7255            compliance: Vec::new(),
7256            sign: String::new(),
7257            unknown_fields: Vec::new(),
7258            loc: Loc {
7259                line: tok.line,
7260                column: tok.column,
7261            },
7262            leading_trivia: Vec::new(),
7263            trailing_trivia: Vec::new(),
7264        };
7265        self.consume(TokenType::LBrace)?;
7266        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7267            let field_name = self.current().value.clone();
7268            let field_loc = Loc {
7269                line: self.current().line,
7270                column: self.current().column,
7271            };
7272            self.advance();
7273            if self.check(TokenType::Colon) {
7274                self.advance();
7275                match field_name.as_str() {
7276                    "scan" => node.scan = self.parse_bracketed_identifiers()?,
7277                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
7278                    "on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
7279                    "severity" => node.severity = self.consume_any_ident_or_kw()?.value.clone(),
7280                    "quarantine" => {
7281                        node.quarantine = self.consume(TokenType::StringLit)?.value.clone()
7282                    }
7283                    "max_retries" => node.max_retries = self.parse_optional_int(),
7284                    "confidence_threshold" => {
7285                        node.confidence_threshold = self.parse_optional_float()
7286                    }
7287                    "allow_tools" => node.allow_tools = self.parse_bracketed_identifiers()?,
7288                    "deny_tools" => node.deny_tools = self.parse_bracketed_identifiers()?,
7289                    "sandbox" => {
7290                        node.sandbox = Some(self.consume_any_ident_or_kw()?.value == "true")
7291                    }
7292                    "redact" => node.redact = self.parse_bracketed_identifiers()?,
7293                    "log" => node.log = self.consume_any_ident_or_kw()?.value.clone(),
7294                    "deflect_message" => {
7295                        node.deflect_message = self.consume(TokenType::StringLit)?.value.clone()
7296                    }
7297                    "taint" => node.taint = self.consume_any_ident_or_kw()?.value.clone(),
7298                    // ESK Fase 6.1 — covered regulatory classes.
7299                    "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
7300                    // §Fase 77.a — egress signing algorithm (closed catalog,
7301                    // validated by the checker: `axon-T846`).
7302                    "sign" => node.sign = self.consume_any_ident_or_kw()?.value.clone(),
7303                    // §Fase 77.a — the value is still skipped (leniency
7304                    // preserved) but the NAME is recorded so the checker
7305                    // emits `axon-W010` instead of a silent drop.
7306                    _ => {
7307                        node.unknown_fields.push((field_name.clone(), field_loc));
7308                        self.skip_value()
7309                    }
7310                }
7311            } else if self.check(TokenType::LBrace) {
7312                self.skip_braced_block()?;
7313            }
7314        }
7315        self.consume(TokenType::RBrace)?;
7316        Ok(node)
7317    }
7318
7319    fn parse_pix(&mut self) -> Result<PixDefinition, ParseError> {
7320        let tok = self.consume(TokenType::Pix)?;
7321        let name = self.consume(TokenType::Identifier)?.value;
7322        let mut node = PixDefinition {
7323            name,
7324            source: String::new(),
7325            depth: None,
7326            branching: None,
7327            model: String::new(),
7328            loc: Loc {
7329                line: tok.line,
7330                column: tok.column,
7331            },
7332            leading_trivia: Vec::new(),
7333            trailing_trivia: Vec::new(),
7334        };
7335        self.consume(TokenType::LBrace)?;
7336        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7337            let field_name = self.current().value.clone();
7338            self.advance();
7339            if self.check(TokenType::Colon) {
7340                self.advance();
7341                match field_name.as_str() {
7342                    "source" => node.source = self.consume(TokenType::StringLit)?.value.clone(),
7343                    "depth" => node.depth = self.parse_optional_int(),
7344                    "branching" => node.branching = self.parse_optional_int(),
7345                    "model" => node.model = self.consume_any_ident_or_kw()?.value.clone(),
7346                    _ => self.skip_value(),
7347                }
7348            } else if self.check(TokenType::LBrace) {
7349                self.skip_braced_block()?;
7350            }
7351        }
7352        self.consume(TokenType::RBrace)?;
7353        Ok(node)
7354    }
7355
7356    /// §Fase 62.0 — `ledger <Name> { source, depth, branching, model }`.
7357    /// The append-only audit chain (formerly the Provenance-Index reading of
7358    /// `pix`). Field grammar mirrors `pix` (same shape) but the SEMANTICS are
7359    /// audit, not navigation: `depth` = chain retention, `branching` = Merkle
7360    /// factor, `model` = hash slug (sha256 / blake3 / sha3).
7361    fn parse_ledger(&mut self) -> Result<LedgerDefinition, ParseError> {
7362        let tok = self.consume(TokenType::Ledger)?;
7363        let name = self.consume(TokenType::Identifier)?.value;
7364        let mut node = LedgerDefinition {
7365            name,
7366            source: String::new(),
7367            depth: None,
7368            branching: None,
7369            model: String::new(),
7370            loc: Loc {
7371                line: tok.line,
7372                column: tok.column,
7373            },
7374            leading_trivia: Vec::new(),
7375            trailing_trivia: Vec::new(),
7376        };
7377        self.consume(TokenType::LBrace)?;
7378        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7379            let field_name = self.current().value.clone();
7380            self.advance();
7381            if self.check(TokenType::Colon) {
7382                self.advance();
7383                match field_name.as_str() {
7384                    "source" => node.source = self.consume(TokenType::StringLit)?.value.clone(),
7385                    "depth" => node.depth = self.parse_optional_int(),
7386                    "branching" => node.branching = self.parse_optional_int(),
7387                    "model" => node.model = self.consume_any_ident_or_kw()?.value.clone(),
7388                    _ => self.skip_value(),
7389                }
7390            } else if self.check(TokenType::LBrace) {
7391                self.skip_braced_block()?;
7392            }
7393        }
7394        self.consume(TokenType::RBrace)?;
7395        Ok(node)
7396    }
7397
7398    fn parse_psyche(&mut self) -> Result<PsycheDefinition, ParseError> {
7399        let tok = self.consume(TokenType::Psyche)?;
7400        let name = self.consume(TokenType::Identifier)?.value;
7401        let mut node = PsycheDefinition {
7402            name,
7403            dimensions: Vec::new(),
7404            manifold_noise: None,
7405            manifold_momentum: None,
7406            safety_constraints: Vec::new(),
7407            quantum_enabled: None,
7408            inference_mode: String::new(),
7409            loc: Loc {
7410                line: tok.line,
7411                column: tok.column,
7412            },
7413            leading_trivia: Vec::new(),
7414            trailing_trivia: Vec::new(),
7415        };
7416        self.consume(TokenType::LBrace)?;
7417        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7418            let field_name = self.current().value.clone();
7419            self.advance();
7420            if self.check(TokenType::Colon) {
7421                self.advance();
7422                match field_name.as_str() {
7423                    "dimensions" => node.dimensions = self.parse_bracketed_identifiers()?,
7424                    "manifold_noise" => node.manifold_noise = self.parse_optional_float(),
7425                    "manifold_momentum" => node.manifold_momentum = self.parse_optional_float(),
7426                    // §Fase 119.f — `safety:` is what README §psyche publishes;
7427                    // `safety_constraints:` is what the parser has always taken.
7428                    // One field, two spellings — the `epsilon`/`tolerance`
7429                    // resolution of §119.b.1.
7430                    "safety_constraints" | "safety" => {
7431                        node.safety_constraints = self.parse_bracketed_identifiers()?
7432                    }
7433                    "quantum_enabled" => {
7434                        node.quantum_enabled = Some(self.consume_any_ident_or_kw()?.value == "true")
7435                    }
7436                    "inference_mode" => {
7437                        node.inference_mode = self.consume_any_ident_or_kw()?.value.clone()
7438                    }
7439                    _ => self.skip_value(),
7440                }
7441            } else if self.check(TokenType::LBrace) {
7442                self.skip_braced_block()?;
7443            }
7444        }
7445        self.consume(TokenType::RBrace)?;
7446        Ok(node)
7447    }
7448
7449    fn parse_corpus(&mut self) -> Result<CorpusDefinition, ParseError> {
7450        let tok = self.consume(TokenType::Corpus)?;
7451        let name = self.consume(TokenType::Identifier)?.value;
7452        let mut node = CorpusDefinition {
7453            name,
7454            documents: Vec::new(),
7455            relations: Vec::new(),
7456            adaptive: false,
7457            mcp_server: String::new(),
7458            mcp_resource_uri: String::new(),
7459            store_source: None,
7460            loc: Loc {
7461                line: tok.line,
7462                column: tok.column,
7463            },
7464            leading_trivia: Vec::new(),
7465            trailing_trivia: Vec::new(),
7466        };
7467        // corpus Name from mcp("server", "uri")  — static MCP-bound short form.
7468        // corpus Name from axonstore { documents: S(id,title)  relations: … }  —
7469        // §Fase 64.A dynamic store-sourced MDN graph (falls through to the body).
7470        let mut dynamic = false;
7471        if self.check(TokenType::From) {
7472            self.advance();
7473            if self.check(TokenType::AxonStore) {
7474                self.advance();
7475                dynamic = true;
7476            } else {
7477                self.consume(TokenType::Mcp)?;
7478                self.consume(TokenType::LParen)?;
7479                node.mcp_server = self.consume(TokenType::StringLit)?.value.clone();
7480                self.consume(TokenType::Comma)?;
7481                node.mcp_resource_uri = self.consume(TokenType::StringLit)?.value.clone();
7482                self.consume(TokenType::RParen)?;
7483                return Ok(node);
7484            }
7485        }
7486        self.consume(TokenType::LBrace)?;
7487        // §Fase 64.A — accumulate the store-mapping pieces while the dynamic body
7488        // is parsed; folded into `node.store_source` after the closing brace.
7489        let mut src = CorpusStoreSource {
7490            doc_store: String::new(),
7491            doc_id_col: String::new(),
7492            doc_title_col: String::new(),
7493            edge_store: String::new(),
7494            edge_from_col: String::new(),
7495            edge_to_col: String::new(),
7496            edge_type_col: String::new(),
7497            edge_weight_col: String::new(),
7498            loc: Loc {
7499                line: tok.line,
7500                column: tok.column,
7501            },
7502        };
7503        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7504            let field_name = self.current().value.clone();
7505            self.advance();
7506            if self.check(TokenType::Colon) {
7507                self.advance();
7508                match field_name.as_str() {
7509                    // §Fase 64.A — dynamic: `documents: <DocStore>(id_col, title_col)`.
7510                    "documents" if dynamic => {
7511                        let (store, cols) = self.parse_corpus_store_mapping(2)?;
7512                        src.doc_store = store;
7513                        src.doc_id_col = cols[0].clone();
7514                        src.doc_title_col = cols[1].clone();
7515                    }
7516                    "documents" => node.documents = self.parse_bracketed_identifiers()?,
7517                    // §Fase 64.A — dynamic: `relations: <EdgeStore>(from, to, etype, weight)`.
7518                    "relations" if dynamic => {
7519                        let (store, cols) = self.parse_corpus_store_mapping(4)?;
7520                        src.edge_store = store;
7521                        src.edge_from_col = cols[0].clone();
7522                        src.edge_to_col = cols[1].clone();
7523                        src.edge_type_col = cols[2].clone();
7524                        src.edge_weight_col = cols[3].clone();
7525                    }
7526                    // §Fase 63.A — static typed weighted edges → MDN corpus graph.
7527                    "relations" => node.relations = self.parse_corpus_relations()?,
7528                    // §Fase 63.C — enable the memory endofunctor.
7529                    "adaptive" => node.adaptive = self.consume_any_ident_or_kw()?.value == "true",
7530                    _ => self.skip_value(),
7531                }
7532            } else if self.check(TokenType::LBrace) {
7533                self.skip_braced_block()?;
7534            }
7535        }
7536        self.consume(TokenType::RBrace)?;
7537        if dynamic {
7538            node.store_source = Some(src);
7539        }
7540        Ok(node)
7541    }
7542
7543    /// §Fase 64.A — parse a store-mapping `<StoreName>( col1, col2, … )` of exactly
7544    /// `n` columns. Used by the dynamic store-sourced corpus's `documents:` (2
7545    /// cols: id, title) and `relations:` (4 cols: from, to, etype, weight). The
7546    /// store name is an identifier (a declared `axonstore`); the columns may be
7547    /// keywords (a column could be named `from`/`type`), so they use the
7548    /// keyword-tolerant consumer. The type-checker validates store + columns.
7549    fn parse_corpus_store_mapping(&mut self, n: usize) -> Result<(String, Vec<String>), ParseError> {
7550        let store = self.consume(TokenType::Identifier)?.value.clone();
7551        self.consume(TokenType::LParen)?;
7552        let mut cols = Vec::with_capacity(n);
7553        for i in 0..n {
7554            if i > 0 {
7555                self.consume(TokenType::Comma)?;
7556            }
7557            cols.push(self.consume_any_ident_or_kw()?.value.clone());
7558        }
7559        self.consume(TokenType::RParen)?;
7560        Ok((store, cols))
7561    }
7562
7563    /// §Fase 63.A — parse `relations: [ etype(from, to, weight) … ]`, the typed
7564    /// weighted edges of an MDN corpus graph. Entries are whitespace/newline
7565    /// separated; commas between them are optional. Edge-type validity (closed
7566    /// catalog), document references, and the weight range are checked by the
7567    /// type-checker (`check_corpus`), not here.
7568    fn parse_corpus_relations(&mut self) -> Result<Vec<CorpusRelation>, ParseError> {
7569        let mut out = Vec::new();
7570        self.consume(TokenType::LBracket)?;
7571        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
7572            if self.check(TokenType::Comma) {
7573                self.advance();
7574                continue;
7575            }
7576            let tok = self.current().clone();
7577            let etype = self.consume_any_ident_or_kw()?.value.clone();
7578            self.consume(TokenType::LParen)?;
7579            let from = self.consume_any_ident_or_kw()?.value.clone();
7580            self.consume(TokenType::Comma)?;
7581            let to = self.consume_any_ident_or_kw()?.value.clone();
7582            self.consume(TokenType::Comma)?;
7583            let weight = self.consume_number()?;
7584            self.consume(TokenType::RParen)?;
7585            out.push(CorpusRelation {
7586                etype,
7587                from,
7588                to,
7589                weight,
7590                loc: Loc { line: tok.line, column: tok.column },
7591            });
7592        }
7593        self.consume(TokenType::RBracket)?;
7594        Ok(out)
7595    }
7596
7597    /// §Fase 108.b — the typed dataspace declaration:
7598    ///
7599    /// ```text
7600    /// dataspace <Name> {
7601    ///     column <name>: <Type>
7602    ///     …
7603    /// }
7604    /// ```
7605    ///
7606    /// Until 108.b the body was consumed by `skip_braced_block()` — any
7607    /// content, including garbage, compiled clean and reached nothing.
7608    /// Now each entry must be a `column` field; the declared type is
7609    /// kept RAW here and resolved against the closed 6-type catalog by
7610    /// the type-checker (`axon-T928`), so all schema errors accumulate
7611    /// in a single compile. An unknown body keyword is a parse error
7612    /// (the grammar is closed — the §38 axonstore posture).
7613    fn parse_dataspace(&mut self) -> Result<DataspaceDefinition, ParseError> {
7614        let tok = self.consume(TokenType::Dataspace)?;
7615        let name = self.consume(TokenType::Identifier)?.value;
7616        let mut node = DataspaceDefinition {
7617            name,
7618            columns: Vec::new(),
7619            loc: Loc {
7620                line: tok.line,
7621                column: tok.column,
7622            },
7623            leading_trivia: Vec::new(),
7624            trailing_trivia: Vec::new(),
7625        };
7626        if self.check(TokenType::LBrace) {
7627            self.consume(TokenType::LBrace)?;
7628            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7629                let entry = self.current().clone();
7630                if entry.value != "column" {
7631                    return Err(ParseError {
7632                        message: format!(
7633                            "Unknown entry `{}` in dataspace `{}`. A dataspace body \
7634                             declares its columnar schema: `column <name>: <Type>` \
7635                             (one per line, over the closed type catalog — \
7636                             Text, Int, Float, Bool, Timestamp, Json).",
7637                            entry.value, node.name
7638                        ),
7639                        line: entry.line,
7640                        column: entry.column,
7641                        ..Default::default()
7642                    });
7643                }
7644                self.advance(); // `column`
7645                let col_tok = self.current().clone();
7646                let col_name = self.consume_any_ident_or_kw()?.value.clone();
7647                self.consume(TokenType::Colon)?;
7648                let declared_type = self.consume_any_ident_or_kw()?.value.clone();
7649                node.columns.push(crate::ast::DataspaceColumn {
7650                    name: col_name,
7651                    declared_type,
7652                    loc: Loc {
7653                        line: col_tok.line,
7654                        column: col_tok.column,
7655                    },
7656                });
7657            }
7658            self.consume(TokenType::RBrace)?;
7659        }
7660        Ok(node)
7661    }
7662
7663    fn parse_ots(&mut self) -> Result<OtsDefinition, ParseError> {
7664        let tok = self.consume(TokenType::Ots)?;
7665        let name = self.consume(TokenType::Identifier)?.value;
7666        let mut node = OtsDefinition {
7667            name,
7668            teleology: String::new(),
7669            homotopy_search: String::new(),
7670            loss_function: String::new(),
7671            loc: Loc {
7672                line: tok.line,
7673                column: tok.column,
7674            },
7675            leading_trivia: Vec::new(),
7676            trailing_trivia: Vec::new(),
7677        };
7678        // Skip optional type params <In, Out>
7679        if self.check(TokenType::Lt) {
7680            while !self.check(TokenType::Gt) && !self.check(TokenType::Eof) {
7681                self.advance();
7682            }
7683            if self.check(TokenType::Gt) {
7684                self.advance();
7685            }
7686        }
7687        self.consume(TokenType::LBrace)?;
7688        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7689            let field_name = self.current().value.clone();
7690            self.advance();
7691            if self.check(TokenType::Colon) {
7692                self.advance();
7693                match field_name.as_str() {
7694                    "teleology" => {
7695                        node.teleology = self.consume(TokenType::StringLit)?.value.clone()
7696                    }
7697                    "homotopy_search" => {
7698                        node.homotopy_search = self.consume_any_ident_or_kw()?.value.clone()
7699                    }
7700                    // §Fase 119.c — README's ots blocks write the loss as a bare
7701                    // identifier (`loss_function: SemanticPreservation`, `L2`,
7702                    // `Contrastive`); the parser accepted only a string literal, so
7703                    // all three published blocks failed at this exact token. Both
7704                    // spellings resolve to the same field.
7705                    "loss_function" => {
7706                        node.loss_function = if self.check(TokenType::StringLit) {
7707                            self.consume(TokenType::StringLit)?.value.clone()
7708                        } else {
7709                            self.consume_any_ident_or_kw()?.value.clone()
7710                        }
7711                    }
7712                    _ => self.skip_value(),
7713                }
7714            } else if self.check(TokenType::LBrace) {
7715                self.skip_braced_block()?;
7716            }
7717        }
7718        self.consume(TokenType::RBrace)?;
7719        Ok(node)
7720    }
7721
7722    fn parse_mandate(&mut self) -> Result<MandateDefinition, ParseError> {
7723        let tok = self.consume(TokenType::Mandate)?;
7724        let name = self.consume(TokenType::Identifier)?.value;
7725        let mut node = MandateDefinition {
7726            name,
7727            constraint: String::new(),
7728            kp: None,
7729            ki: None,
7730            kd: None,
7731            tolerance: None,
7732            max_steps: None,
7733            drift_bound: None,
7734            lipschitz: None,
7735            on_violation: String::new(),
7736            loc: Loc {
7737                line: tok.line,
7738                column: tok.column,
7739            },
7740            leading_trivia: Vec::new(),
7741            trailing_trivia: Vec::new(),
7742        };
7743        self.consume(TokenType::LBrace)?;
7744        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7745            let field_name = self.current().value.clone();
7746            self.advance();
7747            if self.check(TokenType::Colon) {
7748                self.advance();
7749                match field_name.as_str() {
7750                    "constraint" => {
7751                        node.constraint = self.consume(TokenType::StringLit)?.value.clone()
7752                    }
7753                    "kp" | "Kp" => node.kp = self.parse_optional_float(),
7754                    "ki" | "Ki" => node.ki = self.parse_optional_float(),
7755                    "kd" | "Kd" => node.kd = self.parse_optional_float(),
7756                    "max_steps" => node.max_steps = self.parse_optional_int(),
7757                    // §Fase 119.b — `epsilon:` is what the README publishes; `tolerance:`
7758                    // is what the parser has always accepted. They are the SAME ε — the
7759                    // convergence band of `Converge(e, ε, N)`. Both spellings resolve here
7760                    // rather than one of them silently vanishing into `skip_value()`.
7761                    "tolerance" | "epsilon" => node.tolerance = self.parse_optional_float(),
7762                    "on_violation" => {
7763                        node.on_violation = self.consume_any_ident_or_kw()?.value.clone()
7764                    }
7765                    _ => self.skip_value(),
7766                }
7767            } else if self.check(TokenType::LBrace) {
7768                // §Fase 119.b — `pid { Kp: 2.0, Ki: 0.3, Kd: 0.1 }`, which is the form
7769                // README §XV publishes and the form every mandate example uses.
7770                //
7771                // THIS BLOCK USED TO BE `skip_braced_block()`. The consequence was not a
7772                // parse error — it was SILENT ACCEPTANCE: `axon check` printed
7773                // "0 errors" and the IR came out with `kp: None, ki: None, kd: None`.
7774                // The developer wrote the published example, the compiler agreed, and the
7775                // ENTIRE CONTROL LAW was discarded between them. A dropped specification
7776                // that reports success is the §111 defect living in the parser.
7777                if field_name == "pid" {
7778                    self.parse_pid_block(&mut node)?;
7779                } else if field_name == "stability" {
7780                    self.parse_stability_block(&mut node)?;
7781                } else {
7782                    self.skip_braced_block()?;
7783                }
7784            }
7785        }
7786        self.consume(TokenType::RBrace)?;
7787        Ok(node)
7788    }
7789
7790    /// §Fase 119.b — `pid { Kp: <f>, Ki: <f>, Kd: <f> }`.
7791    ///
7792    /// The gains of the Cybernetic Refinement Calculus controller
7793    /// (`docs/papers/paper_mandate.md` §3): `u(t) = Kp·e(t) + Ki·∫e + Kd·de/dt`.
7794    /// Accepts both capitalised (`Kp`, the papers' and README's notation) and
7795    /// lower-case spellings, because the flat `kp:` form was already accepted and
7796    /// removing it would break programs that use it.
7797    ///
7798    /// §Fase 119.h — unknown keys inside the block are REFUSED.
7799    ///
7800    /// §119.b left them skipped, reasoning that the enclosing declaration behaves
7801    /// that way and tightening it was a wider decision. Measuring the published
7802    /// 2.84.0 binary showed what that costs, and the cost is not symmetric:
7803    /// misspelling a GAIN is caught (the missing gain fails the sign conditions),
7804    /// but misspelling a BOUND is not — `stability { drift: 0.5, L: 0.25 }`
7805    /// compiles clean, and the mandate is admitted with no Lyapunov floor at all.
7806    /// The typo does not weaken the check, it DELETES it.
7807    ///
7808    /// These two blocks are not like the enclosing declaration. They are closed
7809    /// catalogues of three and two keys, every one of which is a proof obligation,
7810    /// and an unrecognised key here is never a field a later version will use —
7811    /// it is a typo whose price is a silently discharged safety property.
7812    fn parse_pid_block(&mut self, node: &mut MandateDefinition) -> Result<(), ParseError> {
7813        self.consume(TokenType::LBrace)?;
7814        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7815            let key_token = self.current().clone();
7816            let key = key_token.value.clone();
7817            self.advance();
7818            if self.check(TokenType::Colon) {
7819                self.advance();
7820                match key.as_str() {
7821                    "kp" | "Kp" => node.kp = self.parse_optional_float(),
7822                    "ki" | "Ki" => node.ki = self.parse_optional_float(),
7823                    "kd" | "Kd" => node.kd = self.parse_optional_float(),
7824                    _ => {
7825                        return Err(ParseError {
7826                            message: format!(
7827                                "`{key}` is not a gain of the PID controller. The block accepts \
7828                                 exactly `Kp`, `Ki` and `Kd` (lower-case spellings too). \
7829                                 Skipping what it does not recognise would let a typo drop a \
7830                                 gain, and the stability band is computed from all three."
7831                            ),
7832                            line: key_token.line,
7833                            column: key_token.column,
7834                            ..Default::default()
7835                        });
7836                    }
7837                }
7838            }
7839            if self.check(TokenType::Comma) {
7840                self.advance();
7841            }
7842        }
7843        self.consume(TokenType::RBrace)?;
7844        Ok(())
7845    }
7846
7847    /// §Fase 119.b — `stability { D: <f>, L: <f> }`.
7848    ///
7849    /// The declared hypotheses of the mandate's stability theorem: `D` is the
7850    /// drift bound `sup|drift(t)|` (paper_mandate §3), `L` the Lipschitz
7851    /// constant of the refinement map (prompt_opt §6.3). With them declared,
7852    /// the type checker verifies the full band `D < |Kp+Ki+Kd| < 1/L`; without
7853    /// them it can verify only the sign conditions, which the papers show to be
7854    /// necessary but not sufficient. The declaration travels in the IR as a
7855    /// proof obligation for dispatch — the compiler never invents these
7856    /// numbers, because they are measured properties of a backend it cannot
7857    /// see, and fabricating them would make the static check vacuous.
7858    ///
7859    /// An empty block is a PARSE error, not a silent no-op: `stability { }`
7860    /// asserts nothing, can discharge nothing, and the developer who wrote it
7861    /// believed otherwise.
7862    fn parse_stability_block(
7863        &mut self,
7864        node: &mut MandateDefinition,
7865    ) -> Result<(), ParseError> {
7866        let open = self.consume(TokenType::LBrace)?;
7867        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7868            let key_token = self.current().clone();
7869            let key = key_token.value.clone();
7870            self.advance();
7871            if self.check(TokenType::Colon) {
7872                self.advance();
7873                match key.as_str() {
7874                    "D" | "d" | "drift_bound" => {
7875                        node.drift_bound = self.parse_optional_float()
7876                    }
7877                    "L" | "l" | "lipschitz" => node.lipschitz = self.parse_optional_float(),
7878                    // §Fase 119.h — see `parse_pid_block`. This is the arm that
7879                    // was actually dangerous: a dropped bound is a dropped
7880                    // hypothesis, and the theorem it guards then holds vacuously.
7881                    _ => {
7882                        return Err(ParseError {
7883                            message: format!(
7884                                "`{key}` is not a hypothesis of the stability theorem. The block \
7885                                 accepts exactly `D` (the drift bound, also spelled `d` or \
7886                                 `drift_bound`) and `L` (the Lipschitz constant, also `l` or \
7887                                 `lipschitz`). This is an error rather than a skipped key \
7888                                 because a bound that fails to parse is a bound that is not \
7889                                 declared, and the compiler would then verify the band it can \
7890                                 see — the sign conditions — and admit the mandate as if the \
7891                                 rest had been checked."
7892                            ),
7893                            line: key_token.line,
7894                            column: key_token.column,
7895                            ..Default::default()
7896                        });
7897                    }
7898                }
7899            }
7900            if self.check(TokenType::Comma) {
7901                self.advance();
7902            }
7903        }
7904        self.consume(TokenType::RBrace)?;
7905        if node.drift_bound.is_none() && node.lipschitz.is_none() {
7906            return Err(ParseError {
7907                message: "the `stability { }` block declares neither `D` nor `L` — it                           asserts nothing and can discharge nothing. Declare the drift                           bound (`D:`), the Lipschitz constant (`L:`), or both; or remove                           the block."
7908                    .to_string(),
7909                line: open.line,
7910                column: open.column,
7911                ..Default::default()
7912            });
7913        }
7914        Ok(())
7915    }
7916
7917    /// §Fase 111.f — `compute <Name>(p: T, …) -> T { <expr> }`.
7918    ///
7919    /// # What this used to be
7920    ///
7921    /// ```text
7922    /// // Skip optional parameters/return type before brace
7923    /// while !self.check(TokenType::LBrace) { self.advance(); }
7924    /// ```
7925    ///
7926    /// The parameters and the return type were **skipped token by token**, and
7927    /// the brace held only `shield:`. So a `compute` had **no inputs, no output
7928    /// type and no body** — which is why the runtime could do nothing but bind
7929    /// the literal string `"compute:Name(args)"`, and why a downstream step then
7930    /// consumed that text where it expected a number. The README meanwhile
7931    /// promised "native Fast-Path execution bypassing the LLM" **with an O(n)
7932    /// guarantee**.
7933    ///
7934    /// # What it is now
7935    ///
7936    /// A named pure function over the §70 expression language — the closed,
7937    /// total, side-effect-free term algebra the runtime already evaluates
7938    /// natively (`eval_expr`, the same evaluator behind `let`, `grad` and
7939    /// `conditional`). Linear in the term, no model in the loop: the advertised
7940    /// claim, made true rather than louder.
7941    ///
7942    /// The legacy field form (`compute N { shield: G }`) still parses — its body
7943    /// is simply `None`, and applying a bodyless compute is refused (axon-T941)
7944    /// instead of silently binding a placeholder.
7945    fn parse_compute(&mut self) -> Result<ComputeDefinition, ParseError> {
7946        let tok = self.consume(TokenType::Compute)?;
7947        let name = self.consume(TokenType::Identifier)?.value;
7948        let mut node = ComputeDefinition {
7949            name,
7950            shield_ref: String::new(),
7951            parameters: Vec::new(),
7952            return_type: String::new(),
7953            body: None,
7954            loc: Loc {
7955                line: tok.line,
7956                column: tok.column,
7957            },
7958            leading_trivia: Vec::new(),
7959            trailing_trivia: Vec::new(),
7960        };
7961
7962        // `(p: T, q: T)` — the typed parameters (they used to be skipped).
7963        if self.check(TokenType::LParen) {
7964            self.advance();
7965            while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
7966                let ptok = self.current().clone();
7967                let pname = self.consume_any_ident_or_kw()?.value.clone();
7968                self.consume(TokenType::Colon)?;
7969                let ptype = self.parse_type_expr()?;
7970                node.parameters.push(Parameter {
7971                    name: pname,
7972                    type_expr: ptype,
7973                    loc: self.loc_of(&ptok),
7974                });
7975                if self.check(TokenType::Comma) {
7976                    self.advance();
7977                }
7978            }
7979            self.consume(TokenType::RParen)?;
7980        }
7981
7982        // `-> T` — the declared result type.
7983        if self.check(TokenType::Arrow) {
7984            self.advance();
7985            node.return_type = self.consume_any_ident_or_kw()?.value.clone();
7986        }
7987
7988        self.consume(TokenType::LBrace)?;
7989        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7990            // A `<name>:` pair is a legacy field (only `shield:` is meaningful).
7991            // Anything else is THE BODY — a §70 expression.
7992            //
7993            // NOTE: the field name may be a KEYWORD, not just an identifier —
7994            // `shield` is `TokenType::Shield`. Testing only for `Identifier` here
7995            // sent `compute N { shield: G }` (the legacy declaration form, and
7996            // the shape of the shipped canonical program) down the
7997            // expression-parsing path and broke it. Back-compat is not optional:
7998            // an adopter's existing program must keep compiling.
7999            let is_field = self
8000                .tokens
8001                .get(self.pos + 1)
8002                .map(|t| t.ttype == TokenType::Colon)
8003                .unwrap_or(false);
8004            if is_field {
8005                let field_tok = self.current().clone();
8006                let field_name = self.current().value.clone();
8007                self.advance();
8008                self.consume(TokenType::Colon)?;
8009                match field_name.as_str() {
8010                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
8011                    // §Fase 119.o — `input: a (Float), b (Float)`.
8012                    //
8013                    // This is the parameter list EVERY published compute writes,
8014                    // and it was reaching `skip_value()` — silently discarded, so
8015                    // a compute declared this way had no parameters at all and
8016                    // `run_compute_apply` refused it on arity. The typed form
8017                    // `(a: Float, b: Float)` above stays accepted; both fill the
8018                    // same `parameters`, because they are one concept spelled two
8019                    // ways and a second slot would let them disagree.
8020                    "input" => self.parse_compute_input_list(&mut node)?,
8021                    // §Fase 119.o — `output: Float` / `output: PremiumResult`,
8022                    // the field spelling of `-> T`.
8023                    "output" => {
8024                        node.return_type = self.parse_output_type_string()?;
8025                    }
8026                    _ => self.skip_value(),
8027                }
8028                let _ = field_tok;
8029            } else if self.current().value == "logic"
8030                && self
8031                    .tokens
8032                    .get(self.pos + 1)
8033                    .is_some_and(|t| t.ttype == TokenType::LBrace)
8034            {
8035                // §Fase 119.o — `logic { let … return … }`, the body form all
8036                // four published computes write. It used to fall to
8037                // `parse_expr()`, which met the bare word `logic` and produced a
8038                // diagnostic about an expression the author never wrote.
8039                if node.body.is_some() {
8040                    return Err(ParseError {
8041                        message: "compute declares two bodies; a pure function has one result, \
8042                                  and keeping the last silently would discard the first"
8043                            .to_string(),
8044                        line: self.current().line,
8045                        column: self.current().column,
8046                        ..Default::default()
8047                    });
8048                }
8049                node.body = Some(self.parse_logic_block()?);
8050            } else {
8051                node.body = Some(self.parse_expr()?);
8052            }
8053        }
8054        self.consume(TokenType::RBrace)?;
8055        Ok(node)
8056    }
8057
8058    /// §Fase 119.o — `input: base_rate (Float), risk_factor (Float)`.
8059    ///
8060    /// The published spelling inverts the typed form's punctuation: the name
8061    /// comes first and the type rides in parentheses. Both land in
8062    /// `ComputeDefinition::parameters`.
8063    fn parse_compute_input_list(&mut self, node: &mut ComputeDefinition) -> Result<(), ParseError> {
8064        loop {
8065            let ptok = self.current().clone();
8066            let pname = self.consume_any_ident_or_kw()?.value.clone();
8067            // The type is optional in principle; every published compute writes
8068            // it, and a parameter with no declared type cannot be checked, so an
8069            // absent one is recorded as empty rather than invented.
8070            let type_expr = if self.check(TokenType::LParen) {
8071                self.advance();
8072                let t = self.parse_type_expr()?;
8073                self.consume(TokenType::RParen)?;
8074                t
8075            } else {
8076                TypeExpr {
8077                    name: String::new(),
8078                    generic_param: String::new(),
8079                    optional: false,
8080                    loc: self.loc_of(&ptok),
8081                }
8082            };
8083            node.parameters.push(Parameter {
8084                name: pname,
8085                type_expr,
8086                loc: self.loc_of(&ptok),
8087            });
8088            if self.check(TokenType::Comma) {
8089                self.advance();
8090            } else {
8091                break;
8092            }
8093        }
8094        Ok(())
8095    }
8096
8097    /// §Fase 119.o — the `logic { }` body: a chain of `let`s closed by `return`.
8098    ///
8099    /// Lowered to nested [`Expr::Let`] terms, innermost-last, so
8100    /// `let a = e₁  let b = e₂  return e₃` becomes `Let(a, e₁, Let(b, e₂, e₃))`.
8101    /// That is one evaluation per binding — substituting the bindings into the
8102    /// return expression instead would re-evaluate every bound term once per
8103    /// mention.
8104    ///
8105    /// `return` is REQUIRED. A `logic` block whose last statement is a `let`
8106    /// binds names and produces nothing; the compute would then have to invent a
8107    /// result, and inventing the result of a deterministic function is the one
8108    /// thing this primitive exists not to do.
8109    fn parse_logic_block(&mut self) -> Result<Expr, ParseError> {
8110        let open = self.current().clone();
8111        self.advance(); // `logic`
8112        self.consume(TokenType::LBrace)?;
8113
8114        let mut bindings: Vec<(String, Expr)> = Vec::new();
8115        let mut result: Option<Expr> = None;
8116
8117        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8118            if self.check(TokenType::Let) {
8119                if result.is_some() {
8120                    return Err(ParseError {
8121                        message: "a `let` after the `return` in a `logic { }` block is \
8122                                  unreachable — the block's value is already decided. Move it \
8123                                  above the `return`."
8124                            .to_string(),
8125                        line: self.current().line,
8126                        column: self.current().column,
8127                        ..Default::default()
8128                    });
8129                }
8130                self.advance(); // `let`
8131                let name = self.consume_any_ident_or_kw()?.value.clone();
8132                self.consume(TokenType::Assign)?;
8133                bindings.push((name, self.parse_expr()?));
8134            } else if self.check(TokenType::Return) {
8135                self.advance();
8136                result = Some(self.parse_expr()?);
8137            } else {
8138                let bad = self.current().clone();
8139                return Err(ParseError {
8140                    message: format!(
8141                        "unexpected `{}` in a `logic {{ }}` block — it admits only `let <name> = \
8142                         <expr>` bindings and a closing `return <expr>`. `compute` is a PURE \
8143                         function (its own paper: \"pureza categórica de los morfismos \
8144                         funcionales\"), so a statement that could have an effect is refused \
8145                         rather than parsed and dropped.",
8146                        bad.value
8147                    ),
8148                    line: bad.line,
8149                    column: bad.column,
8150                    ..Default::default()
8151                });
8152            }
8153        }
8154        self.consume(TokenType::RBrace)?;
8155
8156        let mut expr = result.ok_or_else(|| ParseError {
8157            message: "a `logic { }` block must end in `return <expr>`. Without it the block binds \
8158                      names and yields nothing, and the compute would have to invent a result — \
8159                      which is precisely what a deterministic primitive must never do."
8160                .to_string(),
8161            line: open.line,
8162            column: open.column,
8163            ..Default::default()
8164        })?;
8165
8166        // Fold innermost-last so the first `let` written is the outermost scope.
8167        for (name, value) in bindings.into_iter().rev() {
8168            expr = Expr::Let {
8169                name,
8170                value: Box::new(value),
8171                body: Box::new(expr),
8172            };
8173        }
8174        Ok(expr)
8175    }
8176
8177    fn parse_daemon(&mut self) -> Result<DaemonDefinition, ParseError> {
8178        let tok = self.consume(TokenType::Daemon)?;
8179        let name = self.consume(TokenType::Identifier)?.value;
8180        let mut node = DaemonDefinition {
8181            name,
8182            goal: String::new(),
8183            tools: Vec::new(),
8184            memory_ref: String::new(),
8185            strategy: String::new(),
8186            on_stuck: String::new(),
8187            shield_ref: String::new(),
8188            window_ref: String::new(),
8189            budget: None,
8190            max_tokens: None,
8191            max_time: String::new(),
8192            max_cost: None,
8193            listeners: Vec::new(),
8194            requires_capabilities: Vec::new(),
8195            loc: Loc {
8196                line: tok.line,
8197                column: tok.column,
8198            },
8199            leading_trivia: Vec::new(),
8200            trailing_trivia: Vec::new(),
8201        };
8202        // Skip optional parameters/return type before brace
8203        while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
8204            self.advance();
8205        }
8206        self.consume(TokenType::LBrace)?;
8207        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8208            let field = self.current().clone();
8209            let field_name = field.value.clone();
8210            self.advance();
8211            if self.check(TokenType::Colon) {
8212                self.advance();
8213                match field_name.as_str() {
8214                    "goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
8215                    "tools" => node.tools = self.parse_bracketed_identifiers()?,
8216                    "memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
8217                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
8218                    "on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
8219                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
8220                    // §Fase 71.c — `window: <WindowName>` temporal binding.
8221                    "window" => node.window_ref = self.consume_any_ident_or_kw()?.value.clone(),
8222                    "max_tokens" => node.max_tokens = self.parse_optional_int(),
8223                    "max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
8224                    "max_cost" => node.max_cost = self.parse_optional_float(),
8225                    // §Fase 52.d — `requires: [cap, …]` capability scope (same
8226                    // closed slug grammar as `axonendpoint requires:`). The
8227                    // enterprise supervisor mints a per-run principal scoped to
8228                    // exactly these (least privilege).
8229                    "requires" => {
8230                        let bracket_tok = self.current().clone();
8231                        let items = self.parse_bracketed_dot_identifiers()?;
8232                        for slug in &items {
8233                            if !is_valid_capability_slug(slug) {
8234                                return Err(ParseError {
8235                                    message: format!(
8236                                        "Invalid capability slug '{slug}' in daemon '{}' \
8237                                         `requires:`. Capability slugs must match \
8238                                         ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
8239                                         lowercase identifiers. Examples: `daemon.run`, \
8240                                         `memory.write`, `flow.execute`.",
8241                                        node.name
8242                                    ),
8243                                    line: bracket_tok.line,
8244                                    column: bracket_tok.column,
8245                                    ..Default::default()
8246                                });
8247                            }
8248                        }
8249                        node.requires_capabilities = items;
8250                    }
8251                    _ => self.skip_value(),
8252                }
8253            } else if field.ttype == TokenType::Listen {
8254                // §λ-L-E Fase 13 D4 — preserve listen blocks for type
8255                // checking.  We backtracked past the `listen` keyword
8256                // by `advance()` above, so reconstruct a synthetic
8257                // listener using the same dual-mode dispatch the flow
8258                // step parser uses (string topic OR typed channel ref).
8259                let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
8260                    (self.consume(TokenType::StringLit)?.value.clone(), false)
8261                } else {
8262                    (self.consume_any_ident_or_kw()?.value.clone(), true)
8263                };
8264                let mut alias = String::new();
8265                if !self.at_declaration_start()
8266                    && !self.check(TokenType::RBrace)
8267                    && !self.check(TokenType::LBrace)
8268                {
8269                    let next = self.current().clone();
8270                    if next.value == "as" || next.ttype == TokenType::As {
8271                        self.advance();
8272                        alias = self.consume_any_ident_or_kw()?.value.clone();
8273                    }
8274                }
8275                let listen_loc = Loc {
8276                    line: field.line,
8277                    column: field.column,
8278                };
8279                // §Fase 52.a — parse the handler body (was skipped). This is
8280                // what makes a `daemon` operational: the body runs per event /
8281                // scheduled tick (e.g. a `listen "cron:…" as tick { run … }`).
8282                let body = self.parse_listener_body()?;
8283                node.listeners.push(ListenStep {
8284                    channel,
8285                    channel_is_ref,
8286                    event_alias: alias,
8287                    body,
8288                    loc: listen_loc,
8289                });
8290            } else if field_name == "budget" && self.check(TokenType::LBrace) {
8291                // §Fase 72.a — the `budget { … }` linear-effect rate-limit block.
8292                node.budget = Some(self.parse_budget_block(field.line, field.column)?);
8293            } else if self.check(TokenType::LBrace) {
8294                self.skip_braced_block()?;
8295            }
8296        }
8297        self.consume(TokenType::RBrace)?;
8298        Ok(node)
8299    }
8300
8301    /// §Fase 114.a — a TOP-LEVEL `budget <Name> { … }`.
8302    ///
8303    /// Same body as the daemon-attached block; what it gains is a **name** and a
8304    /// **scope that is not a daemon**. Until §114, `budget` was a field of `daemon`
8305    /// and of nothing else — so an adopter deploying an HTTP endpoint that calls a
8306    /// vendor tool had **no way in the language to bound how often it did that.**
8307    /// Not "the bound did not work": **the bound could not be written.** And the
8308    /// HTTP endpoint is what people actually deploy.
8309    fn parse_top_level_budget(&mut self) -> Result<BudgetBlock, ParseError> {
8310        let kw = self.consume(TokenType::Budget)?; // `budget`
8311        let name = self.consume(TokenType::Identifier)?.value;
8312        let mut block = self.parse_budget_block(kw.line, kw.column)?;
8313        block.name = name;
8314        Ok(block)
8315    }
8316
8317    /// §Fase 72.a — `budget { <rate|max>: N per <period> on Tool(<X>) … [on_exhausted: <p>] }`.
8318    fn parse_budget_block(&mut self, line: u32, column: u32) -> Result<BudgetBlock, ParseError> {
8319        self.consume(TokenType::LBrace)?;
8320        let mut quotas = Vec::new();
8321        let mut on_exhausted = String::new();
8322        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8323            let field = self.current().clone();
8324            let field_name = self.consume_any_ident_or_kw()?.value;
8325            match field_name.as_str() {
8326                "rate" | "max" => {
8327                    quotas.push(self.parse_budget_quota(field_name, field.line, field.column)?);
8328                }
8329                "on_exhausted" => {
8330                    self.consume(TokenType::Colon)?;
8331                    on_exhausted = self.consume_any_ident_or_kw()?.value;
8332                }
8333                _ => self.skip_value(),
8334            }
8335        }
8336        self.consume(TokenType::RBrace)?;
8337        Ok(BudgetBlock {
8338            name: String::new(),
8339            quotas,
8340            on_exhausted,
8341            loc: Loc { line, column },
8342            leading_trivia: Vec::new(),
8343            trailing_trivia: Vec::new(),
8344        })
8345    }
8346
8347    /// §Fase 72.a — one quota line: `<kind>: <limit> per <period> on Tool(<effect>)`.
8348    /// `kind` (`rate`/`max`) is already consumed by the caller.
8349    fn parse_budget_quota(
8350        &mut self,
8351        kind: String,
8352        line: u32,
8353        column: u32,
8354    ) -> Result<BudgetQuota, ParseError> {
8355        self.consume(TokenType::Colon)?;
8356        let limit = self.consume_number()? as i64;
8357        // `per <period>`
8358        let _per = self.consume_any_ident_or_kw()?; // the `per` keyword
8359        let period = self.consume_any_ident_or_kw()?.value;
8360        // `on Tool(<effect>)`
8361        let _on = self.consume_any_ident_or_kw()?; // the `on` keyword
8362        let _tool = self.consume_any_ident_or_kw()?; // the `Tool` wrapper keyword
8363        self.consume(TokenType::LParen)?;
8364        let effect = self.consume_any_ident_or_kw()?.value;
8365        self.consume(TokenType::RParen)?;
8366        Ok(BudgetQuota {
8367            kind,
8368            limit,
8369            period,
8370            effect,
8371            loc: Loc { line, column },
8372        })
8373    }
8374
8375    fn parse_axonstore(&mut self) -> Result<AxonStoreDefinition, ParseError> {
8376        let tok = self.consume(TokenType::AxonStore)?;
8377        let name = self.consume(TokenType::Identifier)?.value;
8378        let mut node = AxonStoreDefinition {
8379            name,
8380            backend: String::new(),
8381            connection: String::new(),
8382            resource_ref: String::new(),
8383            confidence_floor: None,
8384            isolation: String::new(),
8385            on_breach: String::new(),
8386            capability: String::new(),
8387            class: String::new(),
8388            column_schema: None,
8389            loc: Loc {
8390                line: tok.line,
8391                column: tok.column,
8392            },
8393            leading_trivia: Vec::new(),
8394            trailing_trivia: Vec::new(),
8395        };
8396        self.consume(TokenType::LBrace)?;
8397        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8398            let field = self.current().clone();
8399            let field_name = field.value.clone();
8400            // §Fase 38.b (D1) — `schema:` declaration in three closed
8401            // forms: inline column block, manifest reference (string
8402            // literal), or env-var schema namespace (`env:VAR` —
8403            // unquoted or quoted). Parse the form; the §38.d / §38.e
8404            // type-checker consumes the resulting AST.
8405            if field.ttype == TokenType::Schema {
8406                self.advance();
8407                let parsed = self.parse_store_schema_declaration(&node.name, field.line, field.column)?;
8408                node.column_schema = Some(parsed);
8409                continue;
8410            }
8411            self.advance();
8412            if self.check(TokenType::Colon) {
8413                self.advance();
8414                match field_name.as_str() {
8415                    "backend" => node.backend = self.consume_any_ident_or_kw()?.value.clone(),
8416                    // §Fase 94.a — the secret-class prefix of a
8417                    // `backend: secrets` metadata store. Dotted-identifier
8418                    // form (`class: crm`, `class: crm.oauth`); the
8419                    // secrets-only placement rule + slug shape are
8420                    // `axon-T900` in the type-checker (it needs the
8421                    // resolved `backend:`, which may appear after this
8422                    // field in source order).
8423                    "class" => node.class = self.parse_dotted_identifier()?,
8424                    "connection" => node.connection = self.parse_config_key()?,
8425                    // §Fase 113 — the `resource` this store RUNS ON. When
8426                    // present the store derives its DSN, its POOL SIZE and its
8427                    // sharing discipline from the resource; `connection:`
8428                    // becomes redundant and `axon-T946` refuses declaring both
8429                    // (the same fact, twice, is how the islands happened).
8430                    "resource" => {
8431                        node.resource_ref = self.consume_any_ident_or_kw()?.value.clone()
8432                    }
8433                    "confidence_floor" => node.confidence_floor = self.parse_optional_float(),
8434                    "isolation" => node.isolation = self.consume_any_ident_or_kw()?.value.clone(),
8435                    "on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
8436                    // §Fase 35.j (D11) — Pillar IV: the capability slug
8437                    // required to access this store. Validated against
8438                    // the closed slug grammar shared with `requires:`.
8439                    "capability" => {
8440                        let slug_tok = self.consume(TokenType::StringLit)?.clone();
8441                        if !is_valid_capability_slug(&slug_tok.value) {
8442                            return Err(ParseError {
8443                                message: format!(
8444                                    "Invalid capability slug '{}' in axonstore '{}' \
8445                                     `capability:`. Capability slugs must match \
8446                                     ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
8447                                     lowercase identifiers starting with a letter. Examples: \
8448                                     `admin`, `tenant.read`, `hipaa.phi.read`.",
8449                                    slug_tok.value, node.name
8450                                ),
8451                                line: slug_tok.line,
8452                                column: slug_tok.column,
8453                                ..Default::default()
8454                            });
8455                        }
8456                        node.capability = slug_tok.value.clone();
8457                    }
8458                    _ => self.skip_value(),
8459                }
8460            } else if self.check(TokenType::LBrace) {
8461                self.skip_braced_block()?;
8462            }
8463        }
8464        self.consume(TokenType::RBrace)?;
8465        Ok(node)
8466    }
8467
8468    /// §Fase 38.b (D1) — parse the three closed forms of an `axonstore`
8469    /// `schema:` declaration:
8470    ///
8471    ///   * form (a) **inline** — `schema { col: Type [constraint…], … }`
8472    ///   * form (b) **manifest reference** — `schema: "qualified.name"`
8473    ///     (string literal that does NOT start with `env:`)
8474    ///   * form (c) **env-var schema namespace** — `schema: env:VAR`
8475    ///     (unquoted) OR `schema: "env:VAR"` (quoted; the literal
8476    ///     starts with `env:`)
8477    ///
8478    /// Called immediately AFTER `schema` is consumed.
8479    fn parse_store_schema_declaration(
8480        &mut self,
8481        store_name: &str,
8482        sch_line: u32,
8483        sch_col: u32,
8484    ) -> Result<crate::store_schema::StoreColumnSchema, ParseError> {
8485        use crate::store_schema::{StoreColumn, StoreColumnSchema, StoreColumnType};
8486
8487        // — Form (a) — inline column block: `schema { ... }`. —
8488        if self.check(TokenType::LBrace) {
8489            self.consume(TokenType::LBrace)?;
8490            let mut columns: Vec<StoreColumn> = Vec::new();
8491            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8492                let col_tok = self.current().clone();
8493                let col_name = self.consume_any_ident_or_kw()?.value.clone();
8494                self.consume(TokenType::Colon)?;
8495                let type_tok = self.consume_any_ident_or_kw()?.clone();
8496                let col_type = StoreColumnType::from_token(&type_tok.value).ok_or_else(|| {
8497                    let names = StoreColumnType::all_canonical_names();
8498                    let suggestion =
8499                        crate::smart_suggest::suggest_for(&type_tok.value, &names);
8500                    let suggest_suffix = if suggestion.is_empty() {
8501                        String::new()
8502                    } else {
8503                        format!(" {suggestion}")
8504                    };
8505                    let known = names.join(", ");
8506                    ParseError {
8507                        message: format!(
8508                            "Unknown column type `{}` for column `{}` in \
8509                             axonstore `{}` `schema:` block. The closed \
8510                             v1.38.0 column-type catalog (Fase 38.b D1) \
8511                             is {{{known}}} (plus common lowercase \
8512                             aliases — `int`/`integer`/`int4` for \
8513                             `Int`, `bool`/`boolean` for `Bool`, etc.).\
8514                             {suggest_suffix}",
8515                            type_tok.value, col_name, store_name
8516                        ),
8517                        line: type_tok.line,
8518                        column: type_tok.column,
8519                        ..Default::default()
8520                    }
8521                })?;
8522
8523                // §Fase 73.a (D1) — the OPTIONAL `Json<T>` shape LENS on a
8524                // column. `payload: Json<UserEvent>` records the expected
8525                // struct shape; the lens is a compile-time expectation only
8526                // (the column stays physically `jsonb`, navigated totally at
8527                // runtime — doctrine `open_data_is_total`). The shape's
8528                // well-formedness (T is a declared `type`) is `axon-T840`
8529                // in the type-checker — it needs the symbol table. Here we
8530                // only enforce the STRUCTURAL rule: a `<T>` lens may refine
8531                // ONLY a `Json` / `Jsonb` column — `axon-T841` otherwise.
8532                let mut json_shape: Option<String> = None;
8533                if self.check(TokenType::Lt) {
8534                    self.advance();
8535                    let shape_tok = self.consume_any_ident_or_kw()?.clone();
8536                    self.consume(TokenType::Gt)?;
8537                    if matches!(col_type, StoreColumnType::Json | StoreColumnType::Jsonb) {
8538                        json_shape = Some(shape_tok.value.clone());
8539                    } else {
8540                        return Err(ParseError {
8541                            message: format!(
8542                                "axon-T841 a shape lens `<{shape}>` may refine \
8543                                 only a `Json` / `Jsonb` column, but column \
8544                                 `{col}` in axonstore `{store}` is `{ty}`. Drop \
8545                                 the `<{shape}>` (a rigid column already has a \
8546                                 fixed shape), or change the column type to \
8547                                 `Json<{shape}>` if it carries open documents.",
8548                                shape = shape_tok.value,
8549                                col = col_name,
8550                                store = store_name,
8551                                ty = col_type.canonical_name(),
8552                            ),
8553                            line: shape_tok.line,
8554                            column: shape_tok.column,
8555                            ..Default::default()
8556                        });
8557                    }
8558                }
8559
8560                let mut col = StoreColumn {
8561                    name: col_name,
8562                    col_type,
8563                    json_shape,
8564                    primary_key: false,
8565                    auto_increment: false,
8566                    not_null: false,
8567                    unique: false,
8568                    indexed: false,
8569                    default_value: String::new(),
8570                    // §Fase 38.x.d (D1) — `identity` is now a recognized
8571                    // inline keyword (see the constraint loop below).
8572                    // Defaults to false; set to true when the adopter
8573                    // writes `id: BigInt primary_key identity`.
8574                    identity: false,
8575                    line: col_tok.line,
8576                    column: col_tok.column,
8577                };
8578
8579                // Trailing constraints (position-independent), matching
8580                // the Python `_parse_store_column` surface.
8581                while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8582                    if self.current().ttype != TokenType::Identifier {
8583                        // The next column starts with a non-identifier
8584                        // (rare) — stop the constraint scan.
8585                        break;
8586                    }
8587                    let constraint = self.current().value.clone();
8588                    match constraint.as_str() {
8589                        "primary_key" => {
8590                            col.primary_key = true;
8591                            self.advance();
8592                        }
8593                        "auto_increment" => {
8594                            col.auto_increment = true;
8595                            self.advance();
8596                        }
8597                        "not_null" => {
8598                            col.not_null = true;
8599                            self.advance();
8600                        }
8601                        "unique" => {
8602                            col.unique = true;
8603                            self.advance();
8604                        }
8605                        // §Fase 73.f (D1) — the `index` constraint declares
8606                        // an index as a capability-honest effect (visible to
8607                        // the deploy gate, not a silent DBA action). The
8608                        // backend picks the method from the column type
8609                        // (GIN for a Json/Jsonb column, b-tree otherwise).
8610                        "index" => {
8611                            col.indexed = true;
8612                            self.advance();
8613                        }
8614                        // §Fase 38.x.d (D1) — `identity` marks a column
8615                        // as `GENERATED ALWAYS/BY DEFAULT AS IDENTITY`.
8616                        // Distinct from `auto_increment` (legacy SERIAL
8617                        // via `nextval(...)` default). T803 skips
8618                        // identity columns from the NOT-NULL-omission
8619                        // check because Postgres auto-fills them; the
8620                        // distinction matters because IDENTITY ALWAYS
8621                        // also rejects user-supplied values, where
8622                        // SERIAL accepts them (a future 38.x.e arm in
8623                        // T802 may surface this).
8624                        "identity" => {
8625                            col.identity = true;
8626                            self.advance();
8627                        }
8628                        "default" => {
8629                            self.advance();
8630                            let dv = self.current().clone();
8631                            if matches!(
8632                                dv.ttype,
8633                                TokenType::StringLit
8634                                    | TokenType::Integer
8635                                    | TokenType::Float
8636                            ) {
8637                                col.default_value = dv.value.clone();
8638                                self.advance();
8639                            } else {
8640                                col.default_value =
8641                                    self.consume_any_ident_or_kw()?.value.clone();
8642                            }
8643                        }
8644                        _ => break,
8645                    }
8646                }
8647
8648                columns.push(col);
8649            }
8650            self.consume(TokenType::RBrace)?;
8651            return Ok(StoreColumnSchema::Inline {
8652                columns,
8653                leading_trivia: Vec::new(),
8654                line: sch_line,
8655                column: sch_col,
8656            });
8657        }
8658
8659        // — Forms (b) + (c) require a `:` separator. —
8660        if !self.check(TokenType::Colon) {
8661            let cur = self.current().clone();
8662            return Err(ParseError {
8663                message: format!(
8664                    "axonstore `{store_name}` `schema:` declaration expects \
8665                     `{{ … }}` (inline columns), `: \"manifest.ref\"` \
8666                     (manifest reference), or `: env:VAR` (per-tenant schema \
8667                     namespace). Got `{}` instead.",
8668                    cur.value
8669                ),
8670                line: cur.line,
8671                column: cur.column,
8672                ..Default::default()
8673            });
8674        }
8675        self.consume(TokenType::Colon)?;
8676
8677        // — Form (b) or (c)-quoted — string literal value. —
8678        if self.check(TokenType::StringLit) {
8679            let lit = self.consume(TokenType::StringLit)?.clone();
8680            let value = lit.value.clone();
8681            if let Some(var) = value.strip_prefix("env:") {
8682                let var = var.trim();
8683                if var.is_empty() {
8684                    return Err(ParseError {
8685                        message: format!(
8686                            "axonstore `{store_name}` `schema: \"env:\"` is \
8687                             missing the variable name after the `env:` \
8688                             prefix."
8689                        ),
8690                        line: lit.line,
8691                        column: lit.column,
8692                        ..Default::default()
8693                    });
8694                }
8695                return Ok(StoreColumnSchema::EnvVar {
8696                    var_name: var.to_string(),
8697                    line: sch_line,
8698                    column: sch_col,
8699                });
8700            }
8701            // Plain string → manifest reference.
8702            if value.trim().is_empty() {
8703                return Err(ParseError {
8704                    message: format!(
8705                        "axonstore `{store_name}` `schema:` manifest reference \
8706                         is empty. Expected `\"qualified.name\"` — e.g. \
8707                         `\"public.tenants\"`."
8708                    ),
8709                    line: lit.line,
8710                    column: lit.column,
8711                    ..Default::default()
8712                });
8713            }
8714            return Ok(StoreColumnSchema::ManifestRef {
8715                qualified_name: value,
8716                line: sch_line,
8717                column: sch_col,
8718            });
8719        }
8720
8721        // — Form (c) unquoted — `env:VAR`. The lexer emits `env` as an
8722        //   identifier, then `:`, then the identifier var name. —
8723        let env_tok = self.current().clone();
8724        if env_tok.value == "env" {
8725            self.advance();
8726            if !self.check(TokenType::Colon) {
8727                return Err(ParseError {
8728                    message: format!(
8729                        "axonstore `{store_name}` `schema: env` is missing the \
8730                         `:` separator. Expected `schema: env:VAR`."
8731                    ),
8732                    line: env_tok.line,
8733                    column: env_tok.column,
8734                    ..Default::default()
8735                });
8736            }
8737            self.advance(); // past ':'
8738            let var_tok = self.consume_any_ident_or_kw()?.clone();
8739            if var_tok.value.trim().is_empty() {
8740                return Err(ParseError {
8741                    message: format!(
8742                        "axonstore `{store_name}` `schema: env:` is missing \
8743                         the variable name."
8744                    ),
8745                    line: var_tok.line,
8746                    column: var_tok.column,
8747                    ..Default::default()
8748                });
8749            }
8750            return Ok(StoreColumnSchema::EnvVar {
8751                var_name: var_tok.value.clone(),
8752                line: sch_line,
8753                column: sch_col,
8754            });
8755        }
8756
8757        Err(ParseError {
8758            message: format!(
8759                "axonstore `{store_name}` `schema:` declaration expects \
8760                 `{{ … }}` (inline columns), `\"manifest.ref\"` (manifest \
8761                 reference), or `env:VAR` (per-tenant schema namespace). \
8762                 Got `{}` instead.",
8763                env_tok.value
8764            ),
8765            line: env_tok.line,
8766            column: env_tok.column,
8767            ..Default::default()
8768        })
8769    }
8770
8771    // ── §λ-L-E Fase 1 — Resource primitive ────────────────────────
8772
8773    /// Parse: `resource Name { kind, endpoint, capacity, lifetime, certainty_floor, shield }`.
8774    ///
8775    /// Mirrors `axon.compiler.parser.Parser._parse_resource`. Unknown fields
8776    /// are silently skipped (keeps the grammar forward-compatible).
8777    fn parse_resource(&mut self) -> Result<ResourceDefinition, ParseError> {
8778        let tok = self.consume(TokenType::Resource)?;
8779        let name = self.consume(TokenType::Identifier)?.value;
8780        let mut node = ResourceDefinition {
8781            name,
8782            kind: String::new(),
8783            endpoint: String::new(),
8784            capacity: None,
8785            lifetime: "affine".to_string(),
8786            certainty_floor: None,
8787            shield_ref: String::new(),
8788            within: String::new(),
8789            loc: Loc {
8790                line: tok.line,
8791                column: tok.column,
8792            },
8793            leading_trivia: Vec::new(),
8794            trailing_trivia: Vec::new(),
8795        };
8796        self.consume(TokenType::LBrace)?;
8797        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8798            let field_tok = self.current().clone();
8799            let field_name = field_tok.value.clone();
8800            self.advance();
8801            if !self.check(TokenType::Colon) {
8802                // Tolerate stray brace or unknown layout.
8803                if self.check(TokenType::LBrace) {
8804                    self.skip_braced_block()?;
8805                }
8806                continue;
8807            }
8808            self.advance(); // past ':'
8809            match field_name.as_str() {
8810                "kind" => node.kind = self.consume_any_ident_or_kw()?.value,
8811                // §Fase 113 — `endpoint:` accepts BOTH shapes on purpose:
8812                //   - a dotted config key  (`endpoint: db.main`)      — the law
8813                //   - a string literal     (`endpoint: "postgres://…"`) — the sin
8814                //
8815                // The literal is REFUSED, but by `axon-T944`, not by the parser.
8816                // If it died here the adopter would read "Expected StringLit",
8817                // which explains nothing. The law gets to say why: *URLs and
8818                // credentials never appear in source* — the same sentence
8819                // `axon-T850` has been saying to `upstream.resolve` all along.
8820                //
8821                // A diagnostic that names the rule teaches; one that names the
8822                // token type only tells you the compiler is unhappy.
8823                "endpoint" => {
8824                    node.endpoint = if self.check(TokenType::StringLit) {
8825                        self.consume(TokenType::StringLit)?.value
8826                    } else {
8827                        self.parse_dotted_identifier()?
8828                    };
8829                }
8830                "capacity" => {
8831                    node.capacity = self.parse_optional_int();
8832                }
8833                "lifetime" => {
8834                    let lt_tok = self.consume_any_ident_or_kw()?;
8835                    let lt = lt_tok.value;
8836                    if !matches!(lt.as_str(), "linear" | "affine" | "persistent") {
8837                        return Err(ParseError {
8838                            message: format!(
8839                                "Invalid lifetime '{lt}' in resource '{}' — \
8840                                 expected linear | affine | persistent",
8841                                node.name
8842                            ),
8843                            line: lt_tok.line,
8844                            column: lt_tok.column,
8845                                                    ..Default::default()
8846                        });
8847                    }
8848                    node.lifetime = lt;
8849                }
8850                "certainty_floor" => {
8851                    node.certainty_floor = self.parse_optional_float();
8852                }
8853                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
8854                // §Fase 113 — `within: <fabric>`. ONE field, so a resource
8855                // cannot be in two fabrics: Separation-Logic disjointness is
8856                // unrepresentable rather than verified.
8857                "within" => node.within = self.consume_any_ident_or_kw()?.value,
8858                // §Fase 113 — an unknown field is a HARD ERROR, not a shrug.
8859                //
8860                // This arm used to be `_ => self.skip_value()`. That is the same
8861                // family as §111's root cause (`parse_block_step` →
8862                // `skip_braced_block()`, which silently killed four primitives):
8863                // a misspelled `withn:` would have been swallowed without a
8864                // word, and the resource would have governed nothing while
8865                // looking governed. A field the parser does not know is a field
8866                // the adopter believes in and the compiler does not.
8867                unknown => {
8868                    return Err(ParseError {
8869                        message: format!(
8870                            "Unknown field '{unknown}' in resource '{}' — expected one of: \
8871                             kind, endpoint, capacity, lifetime, certainty_floor, shield, within",
8872                            node.name
8873                        ),
8874                        line: field_tok.line,
8875                        column: field_tok.column,
8876                        ..Default::default()
8877                    });
8878                }
8879            }
8880        }
8881        self.consume(TokenType::RBrace)?;
8882        Ok(node)
8883    }
8884
8885    /// Parse: `fabric Name { provider, region, zones, ephemeral, shield }`.
8886    fn parse_fabric(&mut self) -> Result<FabricDefinition, ParseError> {
8887        let tok = self.consume(TokenType::Fabric)?;
8888        let name = self.consume(TokenType::Identifier)?.value;
8889        let mut node = FabricDefinition {
8890            name,
8891            provider: String::new(),
8892            region: String::new(),
8893            zones: None,
8894            ephemeral: None,
8895            shield_ref: String::new(),
8896            loc: Loc {
8897                line: tok.line,
8898                column: tok.column,
8899            },
8900            leading_trivia: Vec::new(),
8901            trailing_trivia: Vec::new(),
8902        };
8903        self.consume(TokenType::LBrace)?;
8904        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8905            let field_name = self.current().value.clone();
8906            self.advance();
8907            if !self.check(TokenType::Colon) {
8908                if self.check(TokenType::LBrace) {
8909                    self.skip_braced_block()?;
8910                }
8911                continue;
8912            }
8913            self.advance(); // past ':'
8914            match field_name.as_str() {
8915                "provider" => node.provider = self.consume_any_ident_or_kw()?.value,
8916                "region" => node.region = self.consume(TokenType::StringLit)?.value,
8917                "zones" => node.zones = self.parse_optional_int(),
8918                "ephemeral" => {
8919                    let b = self.parse_bool()?;
8920                    node.ephemeral = Some(b);
8921                }
8922                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
8923                _ => self.skip_value(),
8924            }
8925        }
8926        self.consume(TokenType::RBrace)?;
8927        Ok(node)
8928    }
8929
8930    /// Parse: `manifest Name { resources, fabric, region, zones, compliance }`.
8931    fn parse_manifest(&mut self) -> Result<ManifestDefinition, ParseError> {
8932        let tok = self.consume(TokenType::Manifest)?;
8933        let name = self.consume(TokenType::Identifier)?.value;
8934        let mut node = ManifestDefinition {
8935            name,
8936            resources: Vec::new(),
8937            fabric_ref: String::new(),
8938            region: String::new(),
8939            zones: None,
8940            compliance: Vec::new(),
8941            loc: Loc {
8942                line: tok.line,
8943                column: tok.column,
8944            },
8945            leading_trivia: Vec::new(),
8946            trailing_trivia: Vec::new(),
8947        };
8948        self.consume(TokenType::LBrace)?;
8949        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8950            let field_name = self.current().value.clone();
8951            self.advance();
8952            if !self.check(TokenType::Colon) {
8953                if self.check(TokenType::LBrace) {
8954                    self.skip_braced_block()?;
8955                }
8956                continue;
8957            }
8958            self.advance();
8959            match field_name.as_str() {
8960                "resources" => node.resources = self.parse_bracketed_identifiers()?,
8961                "fabric" => node.fabric_ref = self.consume_any_ident_or_kw()?.value,
8962                "region" => node.region = self.consume(TokenType::StringLit)?.value,
8963                "zones" => node.zones = self.parse_optional_int(),
8964                "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
8965                _ => self.skip_value(),
8966            }
8967        }
8968        self.consume(TokenType::RBrace)?;
8969        Ok(node)
8970    }
8971
8972    /// Parse: `observe Name from Manifest { sources, quorum, timeout, on_partition, certainty_floor }`.
8973    fn parse_observe(&mut self) -> Result<ObserveDefinition, ParseError> {
8974        let tok = self.consume(TokenType::Observe)?;
8975        let name = self.consume(TokenType::Identifier)?.value;
8976        // `from <Manifest>` — required per Python grammar.
8977        self.consume(TokenType::From)?;
8978        let target = self.consume(TokenType::Identifier)?.value;
8979        let mut node = ObserveDefinition {
8980            name,
8981            target,
8982            sources: Vec::new(),
8983            quorum: None,
8984            timeout: String::new(),
8985            on_partition: "fail".to_string(),
8986            certainty_floor: None,
8987            loc: Loc {
8988                line: tok.line,
8989                column: tok.column,
8990            },
8991            leading_trivia: Vec::new(),
8992            trailing_trivia: Vec::new(),
8993        };
8994        self.consume(TokenType::LBrace)?;
8995        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8996            let field_name = self.current().value.clone();
8997            self.advance();
8998            if !self.check(TokenType::Colon) {
8999                if self.check(TokenType::LBrace) {
9000                    self.skip_braced_block()?;
9001                }
9002                continue;
9003            }
9004            self.advance();
9005            match field_name.as_str() {
9006                "sources" => node.sources = self.parse_bracketed_identifiers()?,
9007                "quorum" => node.quorum = self.parse_optional_int(),
9008                "timeout" => {
9009                    let t = self.current().clone();
9010                    match t.ttype {
9011                        TokenType::Duration | TokenType::StringLit => {
9012                            self.advance();
9013                            node.timeout = t.value;
9014                        }
9015                        _ => node.timeout = self.consume_any_ident_or_kw()?.value,
9016                    }
9017                }
9018                "on_partition" => {
9019                    let p_tok = self.consume_any_ident_or_kw()?;
9020                    let p = p_tok.value;
9021                    if !matches!(p.as_str(), "fail" | "shield_quarantine") {
9022                        return Err(ParseError {
9023                            message: format!(
9024                                "Invalid on_partition '{p}' in observe '{}' — \
9025                                 expected fail | shield_quarantine",
9026                                node.name
9027                            ),
9028                            line: p_tok.line,
9029                            column: p_tok.column,
9030                                                    ..Default::default()
9031                        });
9032                    }
9033                    node.on_partition = p;
9034                }
9035                "certainty_floor" => node.certainty_floor = self.parse_optional_float(),
9036                _ => self.skip_value(),
9037            }
9038        }
9039        self.consume(TokenType::RBrace)?;
9040        Ok(node)
9041    }
9042
9043    // ── §λ-L-E Fase 3 — Control cognitivo ─────────────────────────
9044
9045    /// Parse: `reconcile Name { observe, threshold, tolerance, on_drift, shield, mandate, max_retries }`.
9046    fn parse_reconcile(&mut self) -> Result<ReconcileDefinition, ParseError> {
9047        let tok = self.consume(TokenType::Reconcile)?;
9048        let name = self.consume(TokenType::Identifier)?.value;
9049        let mut node = ReconcileDefinition {
9050            name,
9051            observe_ref: String::new(),
9052            threshold: None,
9053            tolerance: None,
9054            on_drift: "provision".to_string(),
9055            shield_ref: String::new(),
9056            mandate_ref: String::new(),
9057            max_retries: 3,
9058            loc: Loc {
9059                line: tok.line,
9060                column: tok.column,
9061            },
9062            leading_trivia: Vec::new(),
9063            trailing_trivia: Vec::new(),
9064        };
9065        self.consume(TokenType::LBrace)?;
9066        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9067            let field_name = self.current().value.clone();
9068            self.advance();
9069            if !self.check(TokenType::Colon) {
9070                if self.check(TokenType::LBrace) {
9071                    self.skip_braced_block()?;
9072                }
9073                continue;
9074            }
9075            self.advance();
9076            match field_name.as_str() {
9077                "observe" => node.observe_ref = self.consume_any_ident_or_kw()?.value,
9078                "threshold" => node.threshold = self.parse_optional_float(),
9079                "tolerance" => node.tolerance = self.parse_optional_float(),
9080                "on_drift" => {
9081                    let d_tok = self.consume_any_ident_or_kw()?;
9082                    let d = d_tok.value;
9083                    if !matches!(d.as_str(), "provision" | "alert" | "refine") {
9084                        return Err(ParseError {
9085                            message: format!(
9086                                "Invalid on_drift '{d}' in reconcile '{}' — \
9087                                 expected provision | alert | refine",
9088                                node.name
9089                            ),
9090                            line: d_tok.line,
9091                            column: d_tok.column,
9092                                                    ..Default::default()
9093                        });
9094                    }
9095                    node.on_drift = d;
9096                }
9097                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
9098                "mandate" => node.mandate_ref = self.consume_any_ident_or_kw()?.value,
9099                "max_retries" => {
9100                    if let Some(v) = self.parse_optional_int() {
9101                        node.max_retries = v;
9102                    }
9103                }
9104                _ => self.skip_value(),
9105            }
9106        }
9107        self.consume(TokenType::RBrace)?;
9108        Ok(node)
9109    }
9110
9111    /// Parse: `lease Name { resource, duration, acquire, on_expire }`.
9112    fn parse_lease(&mut self) -> Result<LeaseDefinition, ParseError> {
9113        let tok = self.consume(TokenType::Lease)?;
9114        let name = self.consume(TokenType::Identifier)?.value;
9115        let mut node = LeaseDefinition {
9116            name,
9117            resource_ref: String::new(),
9118            duration: String::new(),
9119            acquire: "on_start".to_string(),
9120            on_expire: "anchor_breach".to_string(),
9121            loc: Loc {
9122                line: tok.line,
9123                column: tok.column,
9124            },
9125            leading_trivia: Vec::new(),
9126            trailing_trivia: Vec::new(),
9127        };
9128        self.consume(TokenType::LBrace)?;
9129        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9130            let field_name = self.current().value.clone();
9131            self.advance();
9132            if !self.check(TokenType::Colon) {
9133                if self.check(TokenType::LBrace) {
9134                    self.skip_braced_block()?;
9135                }
9136                continue;
9137            }
9138            self.advance();
9139            match field_name.as_str() {
9140                "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
9141                "duration" => {
9142                    let t = self.current().clone();
9143                    match t.ttype {
9144                        TokenType::Duration | TokenType::StringLit => {
9145                            self.advance();
9146                            node.duration = t.value;
9147                        }
9148                        _ => node.duration = self.consume_any_ident_or_kw()?.value,
9149                    }
9150                }
9151                "acquire" => {
9152                    let a_tok = self.consume_any_ident_or_kw()?;
9153                    let a = a_tok.value;
9154                    if !matches!(a.as_str(), "on_start" | "on_demand") {
9155                        return Err(ParseError {
9156                            message: format!(
9157                                "Invalid acquire '{a}' in lease '{}' — \
9158                                 expected on_start | on_demand",
9159                                node.name
9160                            ),
9161                            line: a_tok.line,
9162                            column: a_tok.column,
9163                                                    ..Default::default()
9164                        });
9165                    }
9166                    node.acquire = a;
9167                }
9168                "on_expire" => {
9169                    let e_tok = self.consume_any_ident_or_kw()?;
9170                    let e = e_tok.value;
9171                    if !matches!(e.as_str(), "anchor_breach" | "release" | "extend") {
9172                        return Err(ParseError {
9173                            message: format!(
9174                                "Invalid on_expire '{e}' in lease '{}' — \
9175                                 expected anchor_breach | release | extend",
9176                                node.name
9177                            ),
9178                            line: e_tok.line,
9179                            column: e_tok.column,
9180                                                    ..Default::default()
9181                        });
9182                    }
9183                    node.on_expire = e;
9184                }
9185                _ => self.skip_value(),
9186            }
9187        }
9188        self.consume(TokenType::RBrace)?;
9189        Ok(node)
9190    }
9191
9192    /// Parse: `ensemble Name { observations, quorum, aggregation, certainty_mode }`.
9193    fn parse_ensemble(&mut self) -> Result<EnsembleDefinition, ParseError> {
9194        let tok = self.consume(TokenType::Ensemble)?;
9195        let name = self.consume(TokenType::Identifier)?.value;
9196        let mut node = EnsembleDefinition {
9197            name,
9198            observations: Vec::new(),
9199            quorum: None,
9200            aggregation: "majority".to_string(),
9201            certainty_mode: "min".to_string(),
9202            loc: Loc {
9203                line: tok.line,
9204                column: tok.column,
9205            },
9206            leading_trivia: Vec::new(),
9207            trailing_trivia: Vec::new(),
9208        };
9209        self.consume(TokenType::LBrace)?;
9210        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9211            let field_name = self.current().value.clone();
9212            self.advance();
9213            if !self.check(TokenType::Colon) {
9214                if self.check(TokenType::LBrace) {
9215                    self.skip_braced_block()?;
9216                }
9217                continue;
9218            }
9219            self.advance();
9220            match field_name.as_str() {
9221                "observations" => node.observations = self.parse_bracketed_identifiers()?,
9222                "quorum" => node.quorum = self.parse_optional_int(),
9223                "aggregation" => {
9224                    let a_tok = self.consume_any_ident_or_kw()?;
9225                    let a = a_tok.value;
9226                    if !matches!(a.as_str(), "majority" | "weighted" | "byzantine") {
9227                        return Err(ParseError {
9228                            message: format!(
9229                                "Invalid aggregation '{a}' in ensemble '{}' — \
9230                                 expected majority | weighted | byzantine",
9231                                node.name
9232                            ),
9233                            line: a_tok.line,
9234                            column: a_tok.column,
9235                                                    ..Default::default()
9236                        });
9237                    }
9238                    node.aggregation = a;
9239                }
9240                "certainty_mode" => {
9241                    let c_tok = self.consume_any_ident_or_kw()?;
9242                    let c = c_tok.value;
9243                    if !matches!(c.as_str(), "min" | "weighted" | "harmonic") {
9244                        return Err(ParseError {
9245                            message: format!(
9246                                "Invalid certainty_mode '{c}' in ensemble '{}' — \
9247                                 expected min | weighted | harmonic",
9248                                node.name
9249                            ),
9250                            line: c_tok.line,
9251                            column: c_tok.column,
9252                                                    ..Default::default()
9253                        });
9254                    }
9255                    node.certainty_mode = c;
9256                }
9257                _ => self.skip_value(),
9258            }
9259        }
9260        self.consume(TokenType::RBrace)?;
9261        Ok(node)
9262    }
9263
9264    // ── §λ-L-E Fase 4 — Topology + π-calculus binary sessions ─────
9265
9266    /// Parse: `session Name { role1: [step, …]  role2: [step, …] }`.
9267    ///
9268    /// The enclosing `parse_session_definition` disambiguates from the session
9269    /// step token `session` (which does not exist) by always entering from the
9270    /// top-level dispatch; the identifier role name is consumed after `{`.
9271    fn parse_session_definition(&mut self) -> Result<SessionDefinition, ParseError> {
9272        let tok = self.consume(TokenType::Session)?;
9273        let name = self.consume(TokenType::Identifier)?.value;
9274        let mut node = SessionDefinition {
9275            name,
9276            roles: Vec::new(),
9277            loc: Loc {
9278                line: tok.line,
9279                column: tok.column,
9280            },
9281            leading_trivia: Vec::new(),
9282            trailing_trivia: Vec::new(),
9283        };
9284        self.consume(TokenType::LBrace)?;
9285        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9286            let role_tok = self.consume_any_ident_or_kw()?;
9287            self.consume(TokenType::Colon)?;
9288            let steps = self.parse_session_steps()?;
9289            node.roles.push(SessionRole {
9290                name: role_tok.value,
9291                steps,
9292                loc: Loc {
9293                    line: role_tok.line,
9294                    column: role_tok.column,
9295                },
9296            });
9297        }
9298        self.consume(TokenType::RBrace)?;
9299        Ok(node)
9300    }
9301
9302    /// §Fase 51.c.2 — Parse a Pauli-sum observable declaration:
9303    /// ```text
9304    /// observable EnergyHamiltonian {
9305    ///     qubits: 2
9306    ///     term: 0.5 * "ZZ"
9307    ///     term: -1.2 * "XI"
9308    /// }
9309    /// ```
9310    /// `term:` is a repeatable key (one `cₖ · Pₖ` per line). The coefficient is
9311    /// a real scalar (optional leading `+`/`-`), then `*`, then a quoted Pauli
9312    /// string. The type-checker (§51.c.2) validates the closed `{I,X,Y,Z}`
9313    /// alphabet + equal lengths; real coefficients ⇒ Hermitian by construction.
9314    fn parse_observable(&mut self) -> Result<ObservableDefinition, ParseError> {
9315        let tok = self.consume(TokenType::Observable)?;
9316        let name = self.consume(TokenType::Identifier)?.value;
9317        let mut node = ObservableDefinition {
9318            name,
9319            qubits: None,
9320            terms: Vec::new(),
9321            loc: Loc {
9322                line: tok.line,
9323                column: tok.column,
9324            },
9325            leading_trivia: Vec::new(),
9326            trailing_trivia: Vec::new(),
9327        };
9328        self.consume(TokenType::LBrace)?;
9329        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9330            let key_tok = self.consume_any_ident_or_kw()?;
9331            self.consume(TokenType::Colon)?;
9332            match key_tok.value.as_str() {
9333                "qubits" => node.qubits = Some(self.consume_number()? as i64),
9334                "term" => {
9335                    let term_loc = Loc {
9336                        line: key_tok.line,
9337                        column: key_tok.column,
9338                    };
9339                    // Optional sign, then magnitude.
9340                    let mut negative = false;
9341                    if self.check(TokenType::Minus) {
9342                        self.advance();
9343                        negative = true;
9344                    } else if self.check(TokenType::Plus) {
9345                        self.advance();
9346                    }
9347                    let mag = self.consume_number()?;
9348                    let coefficient = if negative { -mag } else { mag };
9349                    // `*` separator between coefficient and Pauli string.
9350                    self.consume(TokenType::Star)?;
9351                    let pauli = self.consume(TokenType::StringLit)?.value;
9352                    node.terms.push(PauliTerm {
9353                        coefficient,
9354                        pauli,
9355                        loc: term_loc,
9356                    });
9357                }
9358                _ => self.skip_value(),
9359            }
9360        }
9361        self.consume(TokenType::RBrace)?;
9362        Ok(node)
9363    }
9364
9365    /// §Fase 69.a — Parse:
9366    /// `witness Name { claim: <ref>  against: <baseline>  metric: <metric>
9367    ///                 threshold: <ε>  data: <source> }`.
9368    /// Order-free `key: value` pairs. `claim`/`against`/`metric`/`data` are bare
9369    /// identifiers (a ref or a closed-catalog keyword); `threshold` is a number.
9370    /// Well-formedness (known metric, threshold range, required fields) is the
9371    /// type-checker's job (`axon-E0790`).
9372    fn parse_witness(&mut self) -> Result<WitnessDefinition, ParseError> {
9373        let tok = self.consume(TokenType::Witness)?;
9374        let name = self.consume(TokenType::Identifier)?.value;
9375        let mut node = WitnessDefinition {
9376            name,
9377            claim: String::new(),
9378            baseline: String::new(),
9379            metric: String::new(),
9380            threshold: 0.0,
9381            data: String::new(),
9382            loc: Loc {
9383                line: tok.line,
9384                column: tok.column,
9385            },
9386            leading_trivia: Vec::new(),
9387            trailing_trivia: Vec::new(),
9388        };
9389        self.consume(TokenType::LBrace)?;
9390        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9391            let key_tok = self.consume_any_ident_or_kw()?;
9392            self.consume(TokenType::Colon)?;
9393            match key_tok.value.as_str() {
9394                "claim" => node.claim = self.consume_any_ident_or_kw()?.value,
9395                // `against` is the baseline; `against` is not a reserved keyword,
9396                // so it lexes as an identifier key here.
9397                "against" => node.baseline = self.consume_any_ident_or_kw()?.value,
9398                "metric" => node.metric = self.consume_any_ident_or_kw()?.value,
9399                "threshold" => node.threshold = self.consume_number()?,
9400                "data" => node.data = self.consume_any_ident_or_kw()?.value,
9401                _ => self.skip_value(),
9402            }
9403        }
9404        self.consume(TokenType::RBrace)?;
9405        Ok(node)
9406    }
9407
9408    /// §Fase 41.b — Parse:
9409    /// `socket Name { protocol: SessionRef, backpressure: credit(n),
9410    ///               reconnect: cognitive_state, legal_basis: ... }`.
9411    /// Fields are `key: value` pairs (order-free); only `protocol` is required.
9412    fn parse_socket(&mut self) -> Result<SocketDefinition, ParseError> {
9413        let tok = self.consume(TokenType::Socket)?;
9414        let name = self.consume(TokenType::Identifier)?.value;
9415        let mut node = SocketDefinition {
9416            name,
9417            loc: Loc { line: tok.line, column: tok.column },
9418            ..Default::default()
9419        };
9420        self.consume(TokenType::LBrace)?;
9421        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9422            let key = self.consume_any_ident_or_kw()?.value;
9423            self.consume(TokenType::Colon)?;
9424            match key.as_str() {
9425                "protocol" => node.protocol = self.consume_any_ident_or_kw()?.value,
9426                "backpressure" => {
9427                    // `credit(n)` — the typed-resource window.
9428                    let kind = self.consume_any_ident_or_kw()?.value;
9429                    if kind != "credit" {
9430                        return Err(self.error(&format!("expected `credit(n)` for backpressure, got `{kind}`")));
9431                    }
9432                    self.consume(TokenType::LParen)?;
9433                    let n = self
9434                        .consume(TokenType::Integer)?
9435                        .value
9436                        .parse::<i64>()
9437                        .map_err(|_| self.error("backpressure credit must be an integer"))?;
9438                    self.consume(TokenType::RParen)?;
9439                    node.backpressure_credit = Some(n);
9440                }
9441                "reconnect" => {
9442                    let mode = self.consume_any_ident_or_kw()?.value;
9443                    node.reconnect = mode == "cognitive_state";
9444                }
9445                "legal_basis" => node.legal_basis = Some(self.consume_any_ident_or_kw()?.value),
9446                other => return Err(self.error(&format!("unknown socket field `{other}`"))),
9447            }
9448            // Optional comma between fields.
9449            if self.check(TokenType::Comma) {
9450                self.consume(TokenType::Comma)?;
9451            }
9452        }
9453        self.consume(TokenType::RBrace)?;
9454        Ok(node)
9455    }
9456
9457    /// §Fase 80.b — parse `upstream Name [from Preset@vN] { fields }`.
9458    ///
9459    /// Field grammar per `docs/fase/fase_80_upstream_design.md` §1–2. The
9460    /// parser fixes the *shape* only; catalog membership (`transport:`,
9461    /// `auth:`, `overflow:`, `on_exhausted:`), key charsets and projection
9462    /// totality are §80.c type-checker laws (T849–T851), mirroring how
9463    /// `socket` splits parse vs. check.
9464    fn parse_upstream(&mut self) -> Result<UpstreamDefinition, ParseError> {
9465        let tok = self.consume(TokenType::Upstream)?;
9466        let name = self.consume(TokenType::Identifier)?.value;
9467        let mut node = UpstreamDefinition {
9468            name,
9469            loc: Loc { line: tok.line, column: tok.column },
9470            ..Default::default()
9471        };
9472        // §80.f — preset instantiation: `upstream X from DeepgramSTT@v1 {…}`.
9473        if self.check(TokenType::From) {
9474            self.advance();
9475            let base = self.consume(TokenType::Identifier)?.value;
9476            self.consume(TokenType::At)?;
9477            let version = self.consume_any_ident_or_kw()?.value;
9478            node.preset = Some(format!("{base}@{version}"));
9479        }
9480        self.consume(TokenType::LBrace)?;
9481        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9482            let key = self.consume_any_ident_or_kw()?.value;
9483            self.consume(TokenType::Colon)?;
9484            match key.as_str() {
9485                "transport" => node.transport = self.consume_any_ident_or_kw()?.value,
9486                "protocol" => node.protocol = self.consume_any_ident_or_kw()?.value,
9487                "role" => node.role = self.consume_any_ident_or_kw()?.value,
9488                "resolve" => node.resolve = self.parse_dotted_identifier()?,
9489                // §Fase 114.u — the upstream's channel rides a declared
9490                // `resource`; the address + instance bound DERIVE from it.
9491                // XOR with `resolve:` is axon-T951 (type-checker territory).
9492                "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
9493                "secret" => node.secret = self.parse_dotted_identifier()?,
9494                "auth" => {
9495                    // `header("Name")` | `header("Name", "Prefix ")` |
9496                    // `query("param")` | `signed_url`.
9497                    node.auth_kind = self.consume_any_ident_or_kw()?.value;
9498                    if self.check(TokenType::LParen) {
9499                        self.consume(TokenType::LParen)?;
9500                        node.auth_name = Some(self.consume(TokenType::StringLit)?.value);
9501                        if self.check(TokenType::Comma) {
9502                            self.consume(TokenType::Comma)?;
9503                            node.auth_prefix = Some(self.consume(TokenType::StringLit)?.value);
9504                        }
9505                        self.consume(TokenType::RParen)?;
9506                    }
9507                }
9508                "map" => node.map = self.parse_upstream_map()?,
9509                "reconnect" => node.reconnect = Some(self.parse_upstream_reconnect()?),
9510                "overflow" => node.overflow = Some(self.consume_any_ident_or_kw()?.value),
9511                "backpressure" => {
9512                    // `credit(n)` — same typed-resource window as `socket`.
9513                    let kind = self.consume_any_ident_or_kw()?.value;
9514                    if kind != "credit" {
9515                        return Err(self.error(&format!("expected `credit(n)` for backpressure, got `{kind}`")));
9516                    }
9517                    self.consume(TokenType::LParen)?;
9518                    let n = self
9519                        .consume(TokenType::Integer)?
9520                        .value
9521                        .parse::<i64>()
9522                        .map_err(|_| self.error("backpressure credit must be an integer"))?;
9523                    self.consume(TokenType::RParen)?;
9524                    node.backpressure_credit = Some(n);
9525                }
9526                other => return Err(self.error(&format!("unknown upstream field `{other}`"))),
9527            }
9528            // Optional comma between fields.
9529            if self.check(TokenType::Comma) {
9530                self.consume(TokenType::Comma)?;
9531            }
9532        }
9533        self.consume(TokenType::RBrace)?;
9534        Ok(node)
9535    }
9536
9537    /// §Fase 83.a — parse `cors Name { fields }`. Field-shape checks
9538    /// (wildcard+credentials, origin-glob shape, closed method catalog,
9539    /// cross-method path consistency) are §83.c type-checker territory
9540    /// (T853-T857); the parser only builds the structural AST.
9541    ///
9542    /// **Unknown fields are a hard error** (D83.7, not `shield`'s lenient
9543    /// `axon-W010` record-and-skip) — mirrors `upstream`'s stricter
9544    /// posture, appropriate for a security-relevant declaration.
9545    fn parse_cors(&mut self) -> Result<CorsDefinition, ParseError> {
9546        let tok = self.consume(TokenType::Cors)?;
9547        let name = self.consume(TokenType::Identifier)?.value;
9548        let mut node = CorsDefinition {
9549            name,
9550            loc: Loc { line: tok.line, column: tok.column },
9551            ..Default::default()
9552        };
9553        self.consume(TokenType::LBrace)?;
9554        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9555            let key = self.consume_any_ident_or_kw()?.value;
9556            self.consume(TokenType::Colon)?;
9557            match key.as_str() {
9558                "allow_origins" => node.allow_origins = self.parse_bracketed_strings()?,
9559                "allow_methods" => node.allow_methods = self.parse_bracketed_identifiers()?,
9560                "allow_headers" => node.allow_headers = self.parse_bracketed_strings()?,
9561                "allow_credentials" => {
9562                    node.allow_credentials = self.consume_any_ident_or_kw()?.value == "true"
9563                }
9564                "max_age" => node.max_age = Some(self.consume(TokenType::Duration)?.value),
9565                "expose_headers" => node.expose_headers = self.parse_bracketed_strings()?,
9566                other => return Err(self.error(&format!("unknown cors field `{other}`"))),
9567            }
9568            // Optional comma between fields.
9569            if self.check(TokenType::Comma) {
9570                self.consume(TokenType::Comma)?;
9571            }
9572        }
9573        self.consume(TokenType::RBrace)?;
9574        Ok(node)
9575    }
9576
9577    /// §Fase 92.a — parse `credential Name { ttl: grants: }`. Strict
9578    /// closed-catalog (unknown field is a hard error, the §83 D83.7
9579    /// discipline — a credential contract governs AUTHORITY, so a typo can
9580    /// never silently produce a permissive contract). `grants:` slugs are
9581    /// validated at parse time with the same closed grammar as
9582    /// `axonendpoint requires:`; the cross-field laws (non-empty grants,
9583    /// TTL bounds) are §92.a type-checker territory (`axon-T893`/`T894`).
9584    fn parse_credential(&mut self) -> Result<CredentialDefinition, ParseError> {
9585        let tok = self.consume(TokenType::Credential)?;
9586        let name = self.consume(TokenType::Identifier)?.value;
9587        let mut node = CredentialDefinition {
9588            name,
9589            loc: Loc { line: tok.line, column: tok.column },
9590            ..Default::default()
9591        };
9592        self.consume(TokenType::LBrace)?;
9593        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9594            let key = self.consume_any_ident_or_kw()?.value;
9595            self.consume(TokenType::Colon)?;
9596            match key.as_str() {
9597                "ttl" => node.ttl = self.consume(TokenType::Duration)?.value,
9598                "grants" => {
9599                    let bracket_tok = self.current().clone();
9600                    let items = self.parse_bracketed_dot_identifiers()?;
9601                    for slug in &items {
9602                        if !is_valid_capability_slug(slug) {
9603                            return Err(ParseError {
9604                                message: format!(
9605                                    "Invalid capability slug '{slug}' in credential '{}' \
9606                                     `grants:`. Capability slugs must match \
9607                                     ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
9608                                     lowercase identifiers starting with a letter. Examples: \
9609                                     `chat.invoke`, `flow.execute`.",
9610                                    node.name
9611                                ),
9612                                line: bracket_tok.line,
9613                                column: bracket_tok.column,
9614                                ..Default::default()
9615                            });
9616                        }
9617                    }
9618                    node.grants = items;
9619                }
9620                other => return Err(self.error(&format!("unknown credential field `{other}`"))),
9621            }
9622            // Optional comma between fields.
9623            if self.check(TokenType::Comma) {
9624                self.consume(TokenType::Comma)?;
9625            }
9626        }
9627        self.consume(TokenType::RBrace)?;
9628        Ok(node)
9629    }
9630
9631    /// §Fase 85.a — parse `cache Name { backend:, ttl:, key:, default:,
9632    /// apply_to_effects:, invalidate_on: }`. Strict closed-catalog (unknown
9633    /// field is a hard error, the §83 D83.7 discipline — a cache governs
9634    /// correctness, so a typo can never silently mean "no policy"). All
9635    /// cross-field laws (single default, non-pure-needs-ttl, reference
9636    /// resolution, effect widening) are §85.c type-checker territory.
9637    fn parse_cache(&mut self) -> Result<CacheDefinition, ParseError> {
9638        let tok = self.consume(TokenType::Cache)?;
9639        let name = self.consume(TokenType::Identifier)?.value;
9640        let mut node = CacheDefinition {
9641            name,
9642            loc: Loc { line: tok.line, column: tok.column },
9643            ..Default::default()
9644        };
9645        self.consume(TokenType::LBrace)?;
9646        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9647            let key = self.consume_any_ident_or_kw()?.value;
9648            self.consume(TokenType::Colon)?;
9649            match key.as_str() {
9650                "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
9651                "ttl" => node.ttl = Some(self.consume(TokenType::Duration)?.value),
9652                "key" => node.key_params = self.parse_bracketed_identifiers()?,
9653                "default" => {
9654                    node.default_policy = self.consume_any_ident_or_kw()?.value == "true"
9655                }
9656                "apply_to_effects" => {
9657                    node.apply_to_effects = self.parse_bracketed_identifiers()?
9658                }
9659                "invalidate_on" => node.invalidate_on = self.parse_bracketed_identifiers()?,
9660                other => return Err(self.error(&format!("unknown cache field `{other}`"))),
9661            }
9662            if self.check(TokenType::Comma) {
9663                self.consume(TokenType::Comma)?;
9664            }
9665        }
9666        self.consume(TokenType::RBrace)?;
9667        Ok(node)
9668    }
9669
9670    // ── §Fase 99.b — Native Document Synthesis ─────────────────────────────
9671
9672    /// §Fase 99.b — parse `document <Name> { target:, template:?, provenance:?,
9673    /// effects:?, <body blocks> }`. Document-level scalars are handled here;
9674    /// anything of the form `ident { … }` is a body block ([`parse_doc_block_body`]).
9675    /// Unknown scalar fields are a hard error (the §83/§84 closed-catalog
9676    /// discipline); the per-`target` block vocabulary is the §99.c checker's job.
9677    fn parse_document(&mut self) -> Result<crate::ast::DocumentDefinition, ParseError> {
9678        let tok = self.consume(TokenType::Document)?;
9679        let name = self.consume(TokenType::Identifier)?.value;
9680        let mut node = crate::ast::DocumentDefinition {
9681            name,
9682            loc: Loc {
9683                line: tok.line,
9684                column: tok.column,
9685            },
9686            ..Default::default()
9687        };
9688        self.consume(TokenType::LBrace)?;
9689        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9690            let field = self.current().clone();
9691            let field_name = field.value.clone();
9692            self.advance();
9693            if self.check(TokenType::Colon) {
9694                self.advance();
9695                match field_name.as_str() {
9696                    "target" => node.target = self.consume_any_ident_or_kw()?.value,
9697                    "template" => node.template = self.parse_dotted_identifier()?,
9698                    "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
9699                    "effects" => node.effects = Some(self.parse_effect_row()?),
9700                    other => {
9701                        return Err(self.error(&format!(
9702                            "unknown document field `{other}` in document `{}` — expected \
9703                             `target:` / `template:` / `provenance:` / `effects:`, or a body \
9704                             block (`section {{ … }}` / `slide {{ … }}` / `sheet {{ … }}`)",
9705                            node.name
9706                        )))
9707                    }
9708                }
9709            } else if self.check(TokenType::LBrace) {
9710                node.blocks
9711                    .push(self.parse_doc_block_body(field_name, field.line, field.column)?);
9712            } else {
9713                return Err(self.error(&format!(
9714                    "unexpected `{field_name}` in document `{}` body — expected a `field:` or a \
9715                     body block `{field_name} {{ … }}`",
9716                    node.name
9717                )));
9718            }
9719            if self.check(TokenType::Comma) {
9720                self.advance();
9721            }
9722        }
9723        self.consume(TokenType::RBrace)?;
9724        Ok(node)
9725    }
9726
9727    /// §Fase 99.b — parse a document body block whose `kind` was already
9728    /// consumed: `{ (field: value | nested-block { … })* }`. Recursive — a
9729    /// `section` holds `para`/`table`/`chart`; a `slide` holds `bullets`/
9730    /// `notes`; a `sheet` holds `row`/`formula`. A member is a field iff a
9731    /// `:` follows its name; else it must open a nested block (`{`).
9732    fn parse_doc_block_body(
9733        &mut self,
9734        kind: String,
9735        line: u32,
9736        column: u32,
9737    ) -> Result<crate::ast::DocBlock, ParseError> {
9738        let mut block = crate::ast::DocBlock {
9739            kind,
9740            loc: Loc { line, column },
9741            ..Default::default()
9742        };
9743        self.consume(TokenType::LBrace)?;
9744        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9745            let name_tok = self.current().clone();
9746            let name = self.consume_any_ident_or_kw()?.value;
9747            if self.check(TokenType::Colon) {
9748                self.advance();
9749                let value = self.parse_doc_scalar()?;
9750                block.fields.push((name, value));
9751            } else if self.check(TokenType::LBrace) {
9752                let child = self.parse_doc_block_body(name, name_tok.line, name_tok.column)?;
9753                block.children.push(child);
9754            } else {
9755                return Err(self.error(&format!(
9756                    "in document block `{}`: `{name}` must be a `field:` value or open a nested \
9757                     block `{name} {{ … }}`",
9758                    block.kind
9759                )));
9760            }
9761            if self.check(TokenType::Comma) {
9762                self.advance();
9763            }
9764        }
9765        self.consume(TokenType::RBrace)?;
9766        Ok(block)
9767    }
9768
9769    /// §Fase 99.b — parse a document field value into a [`crate::ast::DocScalar`].
9770    /// A bare identifier is a REFERENCE (`text: revenue_summary`) — this is what
9771    /// the assertion-laundering barrier inspects; a quoted string / int / bool /
9772    /// bracketed list are literals.
9773    fn parse_doc_scalar(&mut self) -> Result<crate::ast::DocScalar, ParseError> {
9774        let tok = self.current().clone();
9775        match tok.ttype {
9776            TokenType::StringLit => {
9777                self.advance();
9778                Ok(crate::ast::DocScalar::Text(tok.value))
9779            }
9780            TokenType::Integer => {
9781                self.advance();
9782                Ok(crate::ast::DocScalar::Int(tok.value.parse::<i64>().unwrap_or(0)))
9783            }
9784            TokenType::Bool => {
9785                self.advance();
9786                Ok(crate::ast::DocScalar::Bool(tok.value == "true"))
9787            }
9788            TokenType::LBracket => {
9789                let items = self.parse_bracketed_strings()?;
9790                Ok(crate::ast::DocScalar::List(items))
9791            }
9792            _ => {
9793                let name = self.consume_any_ident_or_kw()?.value;
9794                Ok(crate::ast::DocScalar::Ref(name))
9795            }
9796        }
9797    }
9798
9799    // ── §Fase 105 — Governed CRM Delivery ──────────────────────────────────
9800
9801    /// §Fase 105 — parse `deliver <Name> { target:, provenance:?, secret:,
9802    /// effects:?, <operation blocks> }`. Delivery-level scalars are handled here;
9803    /// anything of the form `ident { … }` is an operation block
9804    /// ([`parse_deliver_op`]). Unknown scalar fields are a hard error (the §99
9805    /// §Fase 110.a — the governed human-notification declaration:
9806    ///
9807    /// ```text
9808    /// notify LowSales {
9809    ///     channel:    sms | whatsapp | telegram
9810    ///     to:         secret(ops.oncall_phone)
9811    ///     template:   "Ventas 7d: ${resumen}"
9812    ///     window:     4h
9813    ///     provenance: attached | cleared
9814    ///     effects:    <web>
9815    /// }
9816    /// ```
9817    ///
9818    /// The closed-field discipline (§99/§105): an unknown scalar field is
9819    /// a hard parse error. The LAWS (T933/T934/T935) live in the checker
9820    /// so violations accumulate; the parser records shape (including a
9821    /// literal `to:` — kept so T934 can refuse it TEACHING the custody
9822    /// form, instead of a bare parse error).
9823    fn parse_notify(&mut self) -> Result<crate::ast::NotifyDefinition, ParseError> {
9824        let tok = self.consume(TokenType::Notify)?;
9825        let name = self.consume(TokenType::Identifier)?.value;
9826        let mut node = crate::ast::NotifyDefinition {
9827            name,
9828            loc: Loc {
9829                line: tok.line,
9830                column: tok.column,
9831            },
9832            ..Default::default()
9833        };
9834        self.consume(TokenType::LBrace)?;
9835        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9836            let field = self.current().clone();
9837            let field_name = field.value.clone();
9838            self.advance();
9839            if self.check(TokenType::Colon) {
9840                self.advance();
9841                match field_name.as_str() {
9842                    "channel" => node.channel = self.consume_any_ident_or_kw()?.value,
9843                    "to" => {
9844                        // The custody form: `secret(<dotted-class>)`. A string
9845                        // literal parses too — the checker refuses it (T934)
9846                        // with the teaching message.
9847                        if self.current().value == "secret" && self.peek_is_lparen() {
9848                            self.advance(); // `secret`
9849                            self.consume(TokenType::LParen)?;
9850                            node.to_secret = self.parse_dotted_identifier()?;
9851                            self.consume(TokenType::RParen)?;
9852                            node.to_is_secret = true;
9853                        } else if self.check(TokenType::StringLit) {
9854                            node.to_secret = self.consume(TokenType::StringLit)?.value.clone();
9855                            node.to_is_secret = false;
9856                        } else {
9857                            node.to_secret = self.consume_any_ident_or_kw()?.value.clone();
9858                            node.to_is_secret = false;
9859                        }
9860                    }
9861                    "template" => {
9862                        node.template = self.consume(TokenType::StringLit)?.value.clone()
9863                    }
9864                    "window" => {
9865                        // `4h` lexes as Integer + ident or one ident — accept
9866                        // both spellings, normalized to the joined form.
9867                        if self.check(TokenType::Integer) {
9868                            let n = self.consume(TokenType::Integer)?.value.clone();
9869                            let unit = self.consume_any_ident_or_kw()?.value.clone();
9870                            node.window = format!("{n}{unit}");
9871                        } else {
9872                            node.window = self.consume_any_ident_or_kw()?.value.clone();
9873                        }
9874                    }
9875                    "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
9876                    "effects" => node.effects = Some(self.parse_effect_row()?),
9877                    other => {
9878                        return Err(self.error(&format!(
9879                            "unknown notify field `{other}` in notify `{}` — expected \
9880                             `channel:` / `to:` / `template:` / `window:` / `provenance:` / \
9881                             `effects:`",
9882                            node.name
9883                        )))
9884                    }
9885                }
9886            }
9887        }
9888        self.consume(TokenType::RBrace)?;
9889        Ok(node)
9890    }
9891
9892    /// §Fase 110.a — one-token lookahead helper for the `secret(` form.
9893    /// §Fase 114.a — is the NEXT token an identifier? (`budget <Name> { … }` vs
9894    /// a bare `budget` used as an ordinary identifier.)
9895    fn peek_is_identifier(&self) -> bool {
9896        self.tokens
9897            .get(self.pos + 1)
9898            .map(|t| t.ttype == TokenType::Identifier)
9899            .unwrap_or(false)
9900    }
9901
9902    fn peek_is_lparen(&self) -> bool {
9903        self.tokens
9904            .get(self.pos + 1)
9905            .map(|t| t.ttype == TokenType::LParen)
9906            .unwrap_or(false)
9907    }
9908
9909    /// closed-catalog discipline); the operation vocabulary is the checker's job.
9910    fn parse_deliver(&mut self) -> Result<crate::ast::DeliverDefinition, ParseError> {
9911        let tok = self.consume(TokenType::Deliver)?;
9912        let name = self.consume(TokenType::Identifier)?.value;
9913        let mut node = crate::ast::DeliverDefinition {
9914            name,
9915            loc: Loc {
9916                line: tok.line,
9917                column: tok.column,
9918            },
9919            ..Default::default()
9920        };
9921        self.consume(TokenType::LBrace)?;
9922        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9923            let field = self.current().clone();
9924            let field_name = field.value.clone();
9925            self.advance();
9926            if self.check(TokenType::Colon) {
9927                self.advance();
9928                match field_name.as_str() {
9929                    "target" => node.target = self.consume_any_ident_or_kw()?.value,
9930                    "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
9931                    "secret" => node.secret = self.consume_any_ident_or_kw()?.value,
9932                    "effects" => node.effects = Some(self.parse_effect_row()?),
9933                    other => {
9934                        return Err(self.error(&format!(
9935                            "unknown deliver field `{other}` in deliver `{}` — expected \
9936                             `target:` / `provenance:` / `secret:` / `effects:`, or an operation \
9937                             block (`upsert_contact {{ … }}` / `create_deal {{ … }}` / \
9938                             `add_note {{ … }}`)",
9939                            node.name
9940                        )))
9941                    }
9942                }
9943            } else if self.check(TokenType::LBrace) {
9944                node.ops
9945                    .push(self.parse_deliver_op(field_name, field.line, field.column)?);
9946            } else {
9947                return Err(self.error(&format!(
9948                    "unexpected `{field_name}` in deliver `{}` body — expected a `field:` or an \
9949                     operation block `{field_name} {{ … }}`",
9950                    node.name
9951                )));
9952            }
9953            if self.check(TokenType::Comma) {
9954                self.advance();
9955            }
9956        }
9957        self.consume(TokenType::RBrace)?;
9958        Ok(node)
9959    }
9960
9961    /// §Fase 105 — parse a delivery operation block whose `kind` was already
9962    /// consumed: `{ (field: value)* }`. Flat (unlike a document block, an
9963    /// operation has no nested children) — each member must be a `field: value`.
9964    fn parse_deliver_op(
9965        &mut self,
9966        kind: String,
9967        line: u32,
9968        column: u32,
9969    ) -> Result<crate::ast::DeliverOp, ParseError> {
9970        let mut op = crate::ast::DeliverOp {
9971            kind,
9972            loc: Loc { line, column },
9973            ..Default::default()
9974        };
9975        self.consume(TokenType::LBrace)?;
9976        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9977            let name = self.consume_any_ident_or_kw()?.value;
9978            self.consume(TokenType::Colon).map_err(|_| {
9979                self.error(&format!(
9980                    "in deliver operation `{}`: `{name}` must be a `field: value` pair — an \
9981                     operation binds scalar fields, it takes no nested blocks",
9982                    op.kind
9983                ))
9984            })?;
9985            let value = self.parse_doc_scalar()?;
9986            op.fields.push((name, value));
9987            if self.check(TokenType::Comma) {
9988                self.advance();
9989            }
9990        }
9991        self.consume(TokenType::RBrace)?;
9992        Ok(op)
9993    }
9994
9995    /// §Fase 87.a — parse `savant <Name> { domain:, cognition{…}, memory{…},
9996    /// budget{…}, mandate <M> {…} … }`. The block surface only; catalog +
9997    /// ref-resolution + budget/interruptibility binding is the §87.b/c checker's
9998    /// job (the standing parse/check split). Unknown fields are a hard error
9999    /// (D83.7): a savant governs an expensive autonomous process.
10000    fn parse_savant(&mut self) -> Result<SavantDefinition, ParseError> {
10001        let tok = self.consume(TokenType::Savant)?;
10002        let name = self.consume(TokenType::Identifier)?.value;
10003        let mut node = SavantDefinition {
10004            name,
10005            loc: Loc {
10006                line: tok.line,
10007                column: tok.column,
10008            },
10009            ..Default::default()
10010        };
10011        self.consume(TokenType::LBrace)?;
10012        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10013            let field = self.current().clone();
10014            let field_name = field.value.clone();
10015            self.advance();
10016            if self.check(TokenType::Colon) {
10017                self.advance();
10018                match field_name.as_str() {
10019                    "domain" => node.domain = self.consume(TokenType::StringLit)?.value,
10020                    other => {
10021                        return Err(self.error(&format!(
10022                            "unknown savant field `{other}` in savant `{}` — expected \
10023                             `domain:` or a `cognition` / `memory` / `budget` / `mandate` block",
10024                            node.name
10025                        )))
10026                    }
10027                }
10028            } else if field_name == "cognition" {
10029                node.cognition = Some(self.parse_savant_cognition(field.line, field.column)?);
10030            } else if field_name == "memory" {
10031                node.memory = Some(self.parse_savant_memory(field.line, field.column)?);
10032            } else if field_name == "budget" {
10033                node.budget = Some(self.parse_savant_budget(field.line, field.column)?);
10034            } else if field_name == "mandate" {
10035                node.mandates
10036                    .push(self.parse_savant_mandate(field.line, field.column)?);
10037            } else {
10038                return Err(self.error(&format!(
10039                    "unexpected `{field_name}` in savant `{}` body — expected `domain:` or a \
10040                     `cognition` / `memory` / `budget` / `mandate` block",
10041                    node.name
10042                )));
10043            }
10044            if self.check(TokenType::Comma) {
10045                self.advance();
10046            }
10047        }
10048        self.consume(TokenType::RBrace)?;
10049        Ok(node)
10050    }
10051
10052    /// §Fase 87.a — the `cognition { depth:, entropic_threshold:, divergence: }`
10053    /// sub-block. Catalog validation of `depth`/`divergence` is §87.b.
10054    fn parse_savant_cognition(
10055        &mut self,
10056        line: u32,
10057        column: u32,
10058    ) -> Result<SavantCognition, ParseError> {
10059        self.consume(TokenType::LBrace)?;
10060        let mut node = SavantCognition {
10061            loc: Loc { line, column },
10062            ..Default::default()
10063        };
10064        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10065            let key = self.consume_any_ident_or_kw()?.value;
10066            self.consume(TokenType::Colon)?;
10067            match key.as_str() {
10068                "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
10069                "entropic_threshold" => node.entropic_threshold = self.parse_optional_float(),
10070                "divergence" => node.divergence = self.consume_any_ident_or_kw()?.value,
10071                other => {
10072                    return Err(self.error(&format!(
10073                        "unknown savant `cognition` field `{other}` — expected \
10074                         `depth` / `entropic_threshold` / `divergence`"
10075                    )))
10076                }
10077            }
10078            if self.check(TokenType::Comma) {
10079                self.advance();
10080            }
10081        }
10082        self.consume(TokenType::RBrace)?;
10083        Ok(node)
10084    }
10085
10086    /// §Fase 87.a — the `memory { backend:, corpus_graph:, isolation_level: }`
10087    /// sub-block. `backend` is resolved to a declared `memory`/`corpus` in §87.c.
10088    fn parse_savant_memory(
10089        &mut self,
10090        line: u32,
10091        column: u32,
10092    ) -> Result<SavantMemory, ParseError> {
10093        self.consume(TokenType::LBrace)?;
10094        let mut node = SavantMemory {
10095            loc: Loc { line, column },
10096            ..Default::default()
10097        };
10098        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10099            let key = self.consume_any_ident_or_kw()?.value;
10100            self.consume(TokenType::Colon)?;
10101            match key.as_str() {
10102                "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
10103                "corpus_graph" => {
10104                    node.corpus_graph = self.consume_any_ident_or_kw()?.value == "true"
10105                }
10106                "isolation_level" => node.isolation_level = self.consume_any_ident_or_kw()?.value,
10107                other => {
10108                    return Err(self.error(&format!(
10109                        "unknown savant `memory` field `{other}` — expected \
10110                         `backend` / `corpus_graph` / `isolation_level`"
10111                    )))
10112                }
10113            }
10114            if self.check(TokenType::Comma) {
10115                self.advance();
10116            }
10117        }
10118        self.consume(TokenType::RBrace)?;
10119        Ok(node)
10120    }
10121
10122    /// §Fase 87.a — the `budget { max_iterations:, max_tool_synth: }` sub-block.
10123    /// Bound to a §72 linear budget (`RateLease`) in §87.c.
10124    fn parse_savant_budget(
10125        &mut self,
10126        line: u32,
10127        column: u32,
10128    ) -> Result<SavantBudget, ParseError> {
10129        self.consume(TokenType::LBrace)?;
10130        let mut node = SavantBudget {
10131            loc: Loc { line, column },
10132            ..Default::default()
10133        };
10134        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10135            let key = self.consume_any_ident_or_kw()?.value;
10136            self.consume(TokenType::Colon)?;
10137            match key.as_str() {
10138                "max_iterations" => node.max_iterations = self.parse_optional_int(),
10139                "max_tool_synth" => node.max_tool_synth = self.parse_optional_int(),
10140                other => {
10141                    return Err(self.error(&format!(
10142                        "unknown savant `budget` field `{other}` — expected \
10143                         `max_iterations` / `max_tool_synth`"
10144                    )))
10145                }
10146            }
10147            if self.check(TokenType::Comma) {
10148                self.advance();
10149            }
10150        }
10151        self.consume(TokenType::RBrace)?;
10152        Ok(node)
10153    }
10154
10155    /// §Fase 87.a — the `mandate <Name> { objective:, output: }` sub-block. The
10156    /// `mandate` keyword is already consumed by `parse_savant`.
10157    fn parse_savant_mandate(
10158        &mut self,
10159        line: u32,
10160        column: u32,
10161    ) -> Result<SavantMandate, ParseError> {
10162        let name = self.consume(TokenType::Identifier)?.value;
10163        let mut node = SavantMandate {
10164            name,
10165            loc: Loc { line, column },
10166            ..Default::default()
10167        };
10168        self.consume(TokenType::LBrace)?;
10169        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10170            let key = self.consume_any_ident_or_kw()?.value;
10171            self.consume(TokenType::Colon)?;
10172            match key.as_str() {
10173                "objective" => node.objective = self.consume(TokenType::StringLit)?.value,
10174                "output" => node.output_type = self.consume_any_ident_or_kw()?.value,
10175                other => {
10176                    return Err(self.error(&format!(
10177                        "unknown savant `mandate` field `{other}` — expected `objective` / `output`"
10178                    )))
10179                }
10180            }
10181            if self.check(TokenType::Comma) {
10182                self.advance();
10183            }
10184        }
10185        self.consume(TokenType::RBrace)?;
10186        Ok(node)
10187    }
10188
10189    /// §Fase 87.d — parse `synth <Name> { target:, risk:, language:, sandbox:,
10190    /// review:, max_lines: }`. Flat key:value block (the `cache` shape). Catalog
10191    /// + deny-by-default validation is §87.d `check_synth`. Unknown fields are a
10192    /// hard error (D83.7): a synth policy governs arbitrary-code execution.
10193    fn parse_synth(&mut self) -> Result<SynthDefinition, ParseError> {
10194        let tok = self.consume(TokenType::Synth)?;
10195        let name = self.consume(TokenType::Identifier)?.value;
10196        let mut node = SynthDefinition {
10197            name,
10198            loc: Loc {
10199                line: tok.line,
10200                column: tok.column,
10201            },
10202            ..Default::default()
10203        };
10204        self.consume(TokenType::LBrace)?;
10205        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10206            let key = self.consume_any_ident_or_kw()?.value;
10207            self.consume(TokenType::Colon)?;
10208            match key.as_str() {
10209                "target" => node.target = self.consume(TokenType::StringLit)?.value,
10210                "risk" => node.risk = self.consume_any_ident_or_kw()?.value,
10211                "language" => node.language = self.consume_any_ident_or_kw()?.value,
10212                "sandbox" => node.sandbox = self.consume_any_ident_or_kw()?.value,
10213                "review" => node.review = self.consume_any_ident_or_kw()?.value,
10214                "max_lines" => node.max_lines = self.parse_optional_int(),
10215                other => {
10216                    return Err(self.error(&format!(
10217                        "unknown synth field `{other}` in synth `{}` — expected `target` / `risk` \
10218                         / `language` / `sandbox` / `review` / `max_lines`",
10219                        node.name
10220                    )))
10221                }
10222            }
10223            if self.check(TokenType::Comma) {
10224                self.consume(TokenType::Comma)?;
10225            }
10226        }
10227        self.consume(TokenType::RBrace)?;
10228        Ok(node)
10229    }
10230
10231    /// §Fase 80.g — parse `voice Name { fields }`. Cross-field laws
10232    /// (stt/tts XOR realtime, interruptible ⇒ legal_basis, ref resolution)
10233    /// are §80.c type-checker territory (T852), same parse/check split as
10234    /// every primitive in this file.
10235    fn parse_voice(&mut self) -> Result<VoiceDefinition, ParseError> {
10236        let tok = self.consume(TokenType::Voice)?;
10237        let name = self.consume(TokenType::Identifier)?.value;
10238        let mut node = VoiceDefinition {
10239            name,
10240            loc: Loc { line: tok.line, column: tok.column },
10241            ..Default::default()
10242        };
10243        self.consume(TokenType::LBrace)?;
10244        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10245            let key = self.consume_any_ident_or_kw()?.value;
10246            self.consume(TokenType::Colon)?;
10247            match key.as_str() {
10248                // Each leg: a declared upstream name or a `Preset@vN` ref.
10249                "stt" => node.stt = Some(self.parse_upstream_ref()?),
10250                "tts" => node.tts = Some(self.parse_upstream_ref()?),
10251                "realtime" => node.realtime = Some(self.parse_upstream_ref()?),
10252                "carrier" => node.carrier = self.consume_any_ident_or_kw()?.value,
10253                "interruptible" => {
10254                    let v = self.consume_any_ident_or_kw()?.value;
10255                    node.interruptible = v == "true";
10256                }
10257                "legal_basis" => node.legal_basis = Some(self.consume_any_ident_or_kw()?.value),
10258                "persona" => node.persona = Some(self.consume(TokenType::Identifier)?.value),
10259                "context" => node.context = Some(self.consume(TokenType::Identifier)?.value),
10260                other => return Err(self.error(&format!("unknown voice field `{other}`"))),
10261            }
10262            if self.check(TokenType::Comma) {
10263                self.consume(TokenType::Comma)?;
10264            }
10265        }
10266        self.consume(TokenType::RBrace)?;
10267        Ok(node)
10268    }
10269
10270    /// §Fase 80.g — an upstream leg reference: `Ident` (a declared
10271    /// `upstream`) or `Ident@vN` (a §80.f preset).
10272    fn parse_upstream_ref(&mut self) -> Result<String, ParseError> {
10273        let base = self.consume(TokenType::Identifier)?.value;
10274        if self.check(TokenType::At) {
10275            self.advance();
10276            let version = self.consume_any_ident_or_kw()?.value;
10277            Ok(format!("{base}@{version}"))
10278        } else {
10279            Ok(base)
10280        }
10281    }
10282
10283    /// §Fase 80.b — parse the `map: [ rule, … ]` projection list.
10284    ///
10285    /// rule := (`send` | `receive`) <MessageType> `as` (`json` | `binary`)
10286    ///         [ `tag` <string> ]                 — send-json only
10287    ///         [ `when` <string> `=` <string> ]   — receive-json only
10288    fn parse_upstream_map(&mut self) -> Result<Vec<UpstreamMapRule>, ParseError> {
10289        self.consume(TokenType::LBracket)?;
10290        let mut rules = Vec::new();
10291        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
10292            let dir_tok = self.current().clone();
10293            let direction = match dir_tok.ttype {
10294                TokenType::Send => "send",
10295                TokenType::Receive => "receive",
10296                _ => {
10297                    return Err(self.error(&format!(
10298                        "upstream map rule must start with `send` or `receive`, got `{}`",
10299                        dir_tok.value
10300                    )))
10301                }
10302            };
10303            self.advance();
10304            let message = self.consume(TokenType::Identifier)?.value;
10305            self.consume(TokenType::As)?;
10306            let framing = self.consume_any_ident_or_kw()?.value;
10307            let mut rule = UpstreamMapRule {
10308                direction: direction.to_string(),
10309                message,
10310                framing,
10311                loc: Loc { line: dir_tok.line, column: dir_tok.column },
10312                ..Default::default()
10313            };
10314            // Optional selectors — contextual identifiers, not keywords.
10315            if self.current().value == "tag" {
10316                self.advance();
10317                rule.tag = Some(self.consume(TokenType::StringLit)?.value);
10318            } else if self.current().value == "when" {
10319                // `when "f" = "v"` — equality discriminator; `when "f"` —
10320                // field-PRESENCE discriminator (vendors like Gemini Live /
10321                // ElevenLabs mark frame kinds by which key exists, not by a
10322                // type value).
10323                self.advance();
10324                rule.when_field = Some(self.consume(TokenType::StringLit)?.value);
10325                if self.check(TokenType::Assign) {
10326                    self.advance();
10327                    rule.when_value = Some(self.consume(TokenType::StringLit)?.value);
10328                }
10329            }
10330            rules.push(rule);
10331            if self.check(TokenType::Comma) {
10332                self.advance();
10333            }
10334        }
10335        self.consume(TokenType::RBracket)?;
10336        Ok(rules)
10337    }
10338
10339    /// §Fase 80.b — parse `reconnect: { backoff_ms: <int>, max_attempts:
10340    /// <int>, on_exhausted: <ident> }` (order-free, all three required —
10341    /// a reconnection policy with a hole is not a policy).
10342    fn parse_upstream_reconnect(&mut self) -> Result<UpstreamReconnect, ParseError> {
10343        self.consume(TokenType::LBrace)?;
10344        let mut backoff_ms: Option<i64> = None;
10345        let mut max_attempts: Option<i64> = None;
10346        let mut on_exhausted: Option<String> = None;
10347        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10348            let key = self.consume_any_ident_or_kw()?.value;
10349            self.consume(TokenType::Colon)?;
10350            match key.as_str() {
10351                "backoff_ms" => {
10352                    backoff_ms = Some(
10353                        self.consume(TokenType::Integer)?
10354                            .value
10355                            .parse::<i64>()
10356                            .map_err(|_| self.error("backoff_ms must be an integer"))?,
10357                    )
10358                }
10359                "max_attempts" => {
10360                    max_attempts = Some(
10361                        self.consume(TokenType::Integer)?
10362                            .value
10363                            .parse::<i64>()
10364                            .map_err(|_| self.error("max_attempts must be an integer"))?,
10365                    )
10366                }
10367                "on_exhausted" => on_exhausted = Some(self.consume_any_ident_or_kw()?.value),
10368                other => return Err(self.error(&format!("unknown reconnect field `{other}`"))),
10369            }
10370            if self.check(TokenType::Comma) {
10371                self.consume(TokenType::Comma)?;
10372            }
10373        }
10374        self.consume(TokenType::RBrace)?;
10375        match (backoff_ms, max_attempts, on_exhausted) {
10376            (Some(b), Some(m), Some(o)) => Ok(UpstreamReconnect { backoff_ms: b, max_attempts: m, on_exhausted: o }),
10377            _ => Err(self.error(
10378                "reconnect requires all of `backoff_ms:`, `max_attempts:`, `on_exhausted:` — a reconnection policy with a hole is not a policy",
10379            )),
10380        }
10381    }
10382
10383    /// Parse: `[send T, receive U, loop, end]`.
10384    fn parse_session_steps(&mut self) -> Result<Vec<SessionStep>, ParseError> {
10385        self.consume(TokenType::LBracket)?;
10386        let mut steps = Vec::new();
10387        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
10388            steps.push(self.parse_session_step()?);
10389            if self.check(TokenType::Comma) {
10390                self.advance();
10391            }
10392        }
10393        self.consume(TokenType::RBracket)?;
10394        Ok(steps)
10395    }
10396
10397    /// §Fase 79.b — a **brace**-delimited session step block: `{ step, step, … }`.
10398    /// Used by the `interrupt`/`resumable` regions (the paper's block surface),
10399    /// as opposed to the `[ … ]` step-lists used by roles and choice arms.
10400    fn parse_session_step_block(&mut self) -> Result<Vec<SessionStep>, ParseError> {
10401        self.consume(TokenType::LBrace)?;
10402        let mut steps = Vec::new();
10403        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10404            steps.push(self.parse_session_step()?);
10405            if self.check(TokenType::Comma) {
10406                self.advance();
10407            }
10408        }
10409        self.consume(TokenType::RBrace)?;
10410        Ok(steps)
10411    }
10412
10413    fn parse_session_step(&mut self) -> Result<SessionStep, ParseError> {
10414        let tok = self.current().clone();
10415        let loc = Loc { line: tok.line, column: tok.column };
10416        match tok.ttype {
10417            TokenType::Send => {
10418                self.advance();
10419                let msg = self.consume_any_ident_or_kw()?;
10420                Ok(SessionStep { op: "send".into(), message_type: msg.value, loc, ..Default::default() })
10421            }
10422            TokenType::Receive => {
10423                self.advance();
10424                let msg = self.consume_any_ident_or_kw()?;
10425                Ok(SessionStep { op: "receive".into(), message_type: msg.value, loc, ..Default::default() })
10426            }
10427            TokenType::Loop => {
10428                self.advance();
10429                Ok(SessionStep { op: "loop".into(), loc, ..Default::default() })
10430            }
10431            TokenType::End => {
10432                self.advance();
10433                Ok(SessionStep { op: "end".into(), loc, ..Default::default() })
10434            }
10435            // §Fase 41.b — choice: `select { ℓ: [..], … }` (⊕) | `branch { ℓ: [..], … }` (&).
10436            // `select`/`branch` are not keywords — they arrive as identifiers.
10437            TokenType::Identifier if tok.value == "select" || tok.value == "branch" => {
10438                self.parse_session_choice(&tok.value, loc)
10439            }
10440            // §Fase 79.b — `interrupt { <body> } on <Signal> as <sig> resumable { <handler> }`.
10441            // Contextual keyword (identifier), like `select`/`branch`.
10442            TokenType::Identifier if tok.value == "interrupt" => {
10443                self.parse_session_interrupt(loc)
10444            }
10445            // §Fase 79.b — `resume`: the handler's normal exit (hand control back to
10446            // the parked body). A bare step, no payload; only meaningful inside an
10447            // `interrupt` handler (enforced at type-check, §79.c).
10448            //
10449            // ⚠️ §Fase 120 — this guard used to require `TokenType::Identifier`,
10450            // and `resume` became a HARD KEYWORD when the algebraic-effect
10451            // constructs landed. The session `resume` is a DIFFERENT `resume`
10452            // (§79.b's interrupt-handler exit, not §120's one-shot continuation
10453            // invocation), and it broke the moment the lexer stopped handing it
10454            // over as an identifier — `axon-frontend/src/voice_desugar.rs`'s own
10455            // expansion source stopped parsing.
10456            //
10457            // Matching on the VALUE rather than the token type is what keeps a
10458            // contextual keyword contextual. This was caught by the corpus gate
10459            // (`fase120_a_effect_grammar::a7_…`), not by review: six new hard
10460            // keywords across a 106-file `.axon` corpus is not a risk anyone
10461            // eyeballs correctly.
10462            _ if tok.value == "resume" => {
10463                self.advance();
10464                Ok(SessionStep { op: "resume".into(), loc, ..Default::default() })
10465            }
10466            _ => Err(ParseError {
10467                message: format!(
10468                    "Invalid session step '{}' — expected send | receive | loop | end | select | branch | interrupt | resume",
10469                    tok.value
10470                ),
10471                line: tok.line,
10472                column: tok.column,
10473                ..Default::default()
10474            }),
10475        }
10476    }
10477
10478    /// §Fase 79.b — consume a **contextual keyword** (`on` / `as` / `resumable`):
10479    /// a token whose *value* must equal `kw`, regardless of whether the lexer
10480    /// classified it as a keyword or a bare identifier. Keeps the `interrupt`
10481    /// surface readable without minting three reserved words.
10482    fn consume_contextual(&mut self, kw: &str) -> Result<(), ParseError> {
10483        let t = self.current().clone();
10484        if t.value != kw {
10485            return Err(ParseError {
10486                message: format!("expected `{kw}` in interrupt step, got `{}`", t.value),
10487                line: t.line,
10488                column: t.column,
10489                ..Default::default()
10490            });
10491        }
10492        self.advance();
10493        Ok(())
10494    }
10495
10496    /// §Fase 79.b — Parse an interruptible region:
10497    /// `interrupt { <body-steps> } on <Signal> as <sig> resumable { <handler-steps> }`.
10498    ///
10499    /// Encoded into the string-tagged `SessionStep` (mirroring the §41.b choice
10500    /// shape): `op = "interrupt"`, `message_type = <Signal>` (validated against the
10501    /// closed `CallInterruptCause` catalog at type-check, §79.c), two labelled
10502    /// `branches` (`body`, `handler`), `binder = <sig>`, `resumable = true`.
10503    fn parse_session_interrupt(&mut self, loc: Loc) -> Result<SessionStep, ParseError> {
10504        self.advance(); // consume `interrupt`
10505        // Body region — a brace-delimited step block (the paper's `interrupt { … }`
10506        // surface; distinct from the `[ … ]` step-lists of roles/choice arms).
10507        let body = self.parse_session_step_block()?;
10508        // `on <Signal>`
10509        self.consume_contextual("on")?;
10510        let signal = self.consume_any_ident_or_kw()?;
10511        // `as <sig>`
10512        self.consume_contextual("as")?;
10513        let binder = self.consume_any_ident_or_kw()?;
10514        // `resumable { <handler> }`
10515        self.consume_contextual("resumable")?;
10516        let handler = self.parse_session_step_block()?;
10517        Ok(SessionStep {
10518            op: "interrupt".into(),
10519            message_type: signal.value,
10520            branches: vec![
10521                SessionBranch { label: "body".into(), steps: body, loc: loc.clone() },
10522                SessionBranch { label: "handler".into(), steps: handler, loc: loc.clone() },
10523            ],
10524            binder: binder.value,
10525            resumable: true,
10526            loc,
10527        })
10528    }
10529
10530    /// §Fase 41.b — Parse a choice step: `select { ask: [..], cancel: [..] }`
10531    /// (or `branch { … }`). Each `label: [steps]` arm is a nested sub-protocol.
10532    fn parse_session_choice(&mut self, op: &str, loc: Loc) -> Result<SessionStep, ParseError> {
10533        self.advance(); // consume `select` / `branch`
10534        self.consume(TokenType::LBrace)?;
10535        let mut branches = Vec::new();
10536        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10537            let label_tok = self.consume_any_ident_or_kw()?;
10538            self.consume(TokenType::Colon)?;
10539            let steps = self.parse_session_steps()?;
10540            branches.push(SessionBranch {
10541                label: label_tok.value,
10542                steps,
10543                loc: Loc { line: label_tok.line, column: label_tok.column },
10544            });
10545            if self.check(TokenType::Comma) {
10546                self.advance();
10547            }
10548        }
10549        self.consume(TokenType::RBrace)?;
10550        Ok(SessionStep { op: op.to_string(), branches, loc, ..Default::default() })
10551    }
10552
10553    /// Parse: `topology Name { nodes: [A, B, …]  edges: [A -> B : Session, …] }`.
10554    fn parse_topology(&mut self) -> Result<TopologyDefinition, ParseError> {
10555        let tok = self.consume(TokenType::Topology)?;
10556        let name = self.consume(TokenType::Identifier)?.value;
10557        let mut node = TopologyDefinition {
10558            name,
10559            nodes: Vec::new(),
10560            edges: Vec::new(),
10561            loc: Loc {
10562                line: tok.line,
10563                column: tok.column,
10564            },
10565            leading_trivia: Vec::new(),
10566            trailing_trivia: Vec::new(),
10567        };
10568        self.consume(TokenType::LBrace)?;
10569        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10570            let field_name = self.current().value.clone();
10571            self.advance();
10572            if !self.check(TokenType::Colon) {
10573                if self.check(TokenType::LBrace) {
10574                    self.skip_braced_block()?;
10575                }
10576                continue;
10577            }
10578            self.advance();
10579            match field_name.as_str() {
10580                "nodes" => node.nodes = self.parse_bracketed_identifiers()?,
10581                "edges" => node.edges = self.parse_topology_edges()?,
10582                _ => self.skip_value(),
10583            }
10584        }
10585        self.consume(TokenType::RBrace)?;
10586        Ok(node)
10587    }
10588
10589    fn parse_topology_edges(&mut self) -> Result<Vec<TopologyEdge>, ParseError> {
10590        self.consume(TokenType::LBracket)?;
10591        let mut edges = Vec::new();
10592        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
10593            edges.push(self.parse_topology_edge()?);
10594            if self.check(TokenType::Comma) {
10595                self.advance();
10596            }
10597        }
10598        self.consume(TokenType::RBracket)?;
10599        Ok(edges)
10600    }
10601
10602    fn parse_topology_edge(&mut self) -> Result<TopologyEdge, ParseError> {
10603        let src_tok = self.consume_any_ident_or_kw()?;
10604        self.consume(TokenType::Arrow)?;
10605        let tgt_tok = self.consume_any_ident_or_kw()?;
10606        self.consume(TokenType::Colon)?;
10607        let sess_tok = self.consume_any_ident_or_kw()?;
10608        Ok(TopologyEdge {
10609            source: src_tok.value,
10610            target: tgt_tok.value,
10611            session_ref: sess_tok.value,
10612            loc: Loc {
10613                line: src_tok.line,
10614                column: src_tok.column,
10615            },
10616        })
10617    }
10618
10619    // ── §λ-L-E Fase 5 — Cognitive immune system (paper_immune_v2.md) ────
10620
10621    /// Parse: `immune Name { watch, sensitivity, baseline, window, scope, tau, decay }`.
10622    fn parse_immune(&mut self) -> Result<ImmuneDefinition, ParseError> {
10623        let tok = self.consume(TokenType::Immune)?;
10624        let name = self.consume(TokenType::Identifier)?.value;
10625        let mut node = ImmuneDefinition {
10626            name,
10627            watch: Vec::new(),
10628            sensitivity: None,
10629            baseline: "learned".to_string(),
10630            window: 100,
10631            scope: String::new(),
10632            tau: String::new(),
10633            decay: "exponential".to_string(),
10634            loc: Loc {
10635                line: tok.line,
10636                column: tok.column,
10637            },
10638            leading_trivia: Vec::new(),
10639            trailing_trivia: Vec::new(),
10640        };
10641        self.consume(TokenType::LBrace)?;
10642        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10643            let field_name = self.current().value.clone();
10644            self.advance();
10645            if !self.check(TokenType::Colon) {
10646                if self.check(TokenType::LBrace) {
10647                    self.skip_braced_block()?;
10648                }
10649                continue;
10650            }
10651            self.advance();
10652            match field_name.as_str() {
10653                "watch" => node.watch = self.parse_bracketed_identifiers()?,
10654                "sensitivity" => node.sensitivity = self.parse_optional_float(),
10655                "baseline" => node.baseline = self.consume_any_ident_or_kw()?.value,
10656                "window" => {
10657                    if let Some(v) = self.parse_optional_int() {
10658                        node.window = v;
10659                    }
10660                }
10661                "scope" => {
10662                    let s_tok = self.consume_any_ident_or_kw()?;
10663                    let s = s_tok.value;
10664                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
10665                        return Err(ParseError {
10666                            message: format!(
10667                                "Invalid scope '{s}' in immune '{}' — \
10668                                 expected tenant | flow | global",
10669                                node.name
10670                            ),
10671                            line: s_tok.line,
10672                            column: s_tok.column,
10673                                                    ..Default::default()
10674                        });
10675                    }
10676                    node.scope = s;
10677                }
10678                "tau" => {
10679                    let t = self.current().clone();
10680                    match t.ttype {
10681                        TokenType::Duration | TokenType::StringLit => {
10682                            self.advance();
10683                            node.tau = t.value;
10684                        }
10685                        _ => node.tau = self.consume_any_ident_or_kw()?.value,
10686                    }
10687                }
10688                "decay" => {
10689                    let d_tok = self.consume_any_ident_or_kw()?;
10690                    let d = d_tok.value;
10691                    if !matches!(d.as_str(), "exponential" | "linear" | "none") {
10692                        return Err(ParseError {
10693                            message: format!(
10694                                "Invalid decay '{d}' in immune '{}' — \
10695                                 expected exponential | linear | none",
10696                                node.name
10697                            ),
10698                            line: d_tok.line,
10699                            column: d_tok.column,
10700                                                    ..Default::default()
10701                        });
10702                    }
10703                    node.decay = d;
10704                }
10705                _ => self.skip_value(),
10706            }
10707        }
10708        self.consume(TokenType::RBrace)?;
10709        Ok(node)
10710    }
10711
10712    /// Parse: `reflex Name { trigger, on_level, action, scope, sla }`.
10713    fn parse_reflex(&mut self) -> Result<ReflexDefinition, ParseError> {
10714        let tok = self.consume(TokenType::Reflex)?;
10715        let name = self.consume(TokenType::Identifier)?.value;
10716        let mut node = ReflexDefinition {
10717            name,
10718            trigger: String::new(),
10719            on_level: "doubt".to_string(),
10720            action: String::new(),
10721            scope: String::new(),
10722            sla: String::new(),
10723            loc: Loc {
10724                line: tok.line,
10725                column: tok.column,
10726            },
10727            leading_trivia: Vec::new(),
10728            trailing_trivia: Vec::new(),
10729        };
10730        self.consume(TokenType::LBrace)?;
10731        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10732            let field_name = self.current().value.clone();
10733            self.advance();
10734            if !self.check(TokenType::Colon) {
10735                if self.check(TokenType::LBrace) {
10736                    self.skip_braced_block()?;
10737                }
10738                continue;
10739            }
10740            self.advance();
10741            match field_name.as_str() {
10742                "trigger" => node.trigger = self.consume_any_ident_or_kw()?.value,
10743                "on_level" => {
10744                    let l_tok = self.consume_any_ident_or_kw()?;
10745                    let l = l_tok.value;
10746                    if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
10747                        return Err(ParseError {
10748                            message: format!(
10749                                "Invalid on_level '{l}' in reflex '{}' — \
10750                                 expected know | believe | speculate | doubt",
10751                                node.name
10752                            ),
10753                            line: l_tok.line,
10754                            column: l_tok.column,
10755                                                    ..Default::default()
10756                        });
10757                    }
10758                    node.on_level = l;
10759                }
10760                "action" => {
10761                    let a_tok = self.consume_any_ident_or_kw()?;
10762                    let a = a_tok.value;
10763                    if !matches!(
10764                        a.as_str(),
10765                        "drop"
10766                            | "revoke"
10767                            | "emit"
10768                            | "redact"
10769                            | "quarantine"
10770                            | "terminate"
10771                            | "alert"
10772                    ) {
10773                        return Err(ParseError {
10774                            message: format!(
10775                                "Invalid action '{a}' in reflex '{}' — \
10776                                 expected drop | revoke | emit | redact | \
10777                                 quarantine | terminate | alert",
10778                                node.name
10779                            ),
10780                            line: a_tok.line,
10781                            column: a_tok.column,
10782                                                    ..Default::default()
10783                        });
10784                    }
10785                    node.action = a;
10786                }
10787                "scope" => {
10788                    let s_tok = self.consume_any_ident_or_kw()?;
10789                    let s = s_tok.value;
10790                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
10791                        return Err(ParseError {
10792                            message: format!(
10793                                "Invalid scope '{s}' in reflex '{}' — \
10794                                 expected tenant | flow | global",
10795                                node.name
10796                            ),
10797                            line: s_tok.line,
10798                            column: s_tok.column,
10799                                                    ..Default::default()
10800                        });
10801                    }
10802                    node.scope = s;
10803                }
10804                "sla" => {
10805                    let t = self.current().clone();
10806                    match t.ttype {
10807                        TokenType::Duration | TokenType::StringLit => {
10808                            self.advance();
10809                            node.sla = t.value;
10810                        }
10811                        _ => node.sla = self.consume_any_ident_or_kw()?.value,
10812                    }
10813                }
10814                _ => self.skip_value(),
10815            }
10816        }
10817        self.consume(TokenType::RBrace)?;
10818        Ok(node)
10819    }
10820
10821    /// Parse: `heal Name { source, on_level, mode, scope, review_sla, shield, max_patches }`.
10822    fn parse_heal(&mut self) -> Result<HealDefinition, ParseError> {
10823        let tok = self.consume(TokenType::Heal)?;
10824        let name = self.consume(TokenType::Identifier)?.value;
10825        let mut node = HealDefinition {
10826            name,
10827            source: String::new(),
10828            on_level: "doubt".to_string(),
10829            mode: "human_in_loop".to_string(),
10830            scope: String::new(),
10831            review_sla: String::new(),
10832            shield_ref: String::new(),
10833            max_patches: 3,
10834            loc: Loc {
10835                line: tok.line,
10836                column: tok.column,
10837            },
10838            leading_trivia: Vec::new(),
10839            trailing_trivia: Vec::new(),
10840        };
10841        self.consume(TokenType::LBrace)?;
10842        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10843            let field_name = self.current().value.clone();
10844            self.advance();
10845            if !self.check(TokenType::Colon) {
10846                if self.check(TokenType::LBrace) {
10847                    self.skip_braced_block()?;
10848                }
10849                continue;
10850            }
10851            self.advance();
10852            match field_name.as_str() {
10853                "source" => node.source = self.consume_any_ident_or_kw()?.value,
10854                "on_level" => {
10855                    let l_tok = self.consume_any_ident_or_kw()?;
10856                    let l = l_tok.value;
10857                    if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
10858                        return Err(ParseError {
10859                            message: format!(
10860                                "Invalid on_level '{l}' in heal '{}' — \
10861                                 expected know | believe | speculate | doubt",
10862                                node.name
10863                            ),
10864                            line: l_tok.line,
10865                            column: l_tok.column,
10866                                                    ..Default::default()
10867                        });
10868                    }
10869                    node.on_level = l;
10870                }
10871                "mode" => {
10872                    let m_tok = self.consume_any_ident_or_kw()?;
10873                    let m = m_tok.value;
10874                    if !matches!(m.as_str(), "audit_only" | "human_in_loop" | "adversarial") {
10875                        return Err(ParseError {
10876                            message: format!(
10877                                "Invalid mode '{m}' in heal '{}' — \
10878                                 expected audit_only | human_in_loop | adversarial",
10879                                node.name
10880                            ),
10881                            line: m_tok.line,
10882                            column: m_tok.column,
10883                                                    ..Default::default()
10884                        });
10885                    }
10886                    node.mode = m;
10887                }
10888                "scope" => {
10889                    let s_tok = self.consume_any_ident_or_kw()?;
10890                    let s = s_tok.value;
10891                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
10892                        return Err(ParseError {
10893                            message: format!(
10894                                "Invalid scope '{s}' in heal '{}' — \
10895                                 expected tenant | flow | global",
10896                                node.name
10897                            ),
10898                            line: s_tok.line,
10899                            column: s_tok.column,
10900                                                    ..Default::default()
10901                        });
10902                    }
10903                    node.scope = s;
10904                }
10905                "review_sla" => {
10906                    let t = self.current().clone();
10907                    match t.ttype {
10908                        TokenType::Duration | TokenType::StringLit => {
10909                            self.advance();
10910                            node.review_sla = t.value;
10911                        }
10912                        _ => node.review_sla = self.consume_any_ident_or_kw()?.value,
10913                    }
10914                }
10915                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
10916                "max_patches" => {
10917                    if let Some(v) = self.parse_optional_int() {
10918                        node.max_patches = v;
10919                    }
10920                }
10921                _ => self.skip_value(),
10922            }
10923        }
10924        self.consume(TokenType::RBrace)?;
10925        Ok(node)
10926    }
10927
10928    // ── §λ-L-E Fase 9 — UI cognitiva (component / view) ────────────
10929
10930    /// Parse: `component Name { renders, via_shield, on_interact, render_hint }`.
10931    fn parse_component(&mut self) -> Result<ComponentDefinition, ParseError> {
10932        let tok = self.consume(TokenType::Component)?;
10933        let name = self.consume(TokenType::Identifier)?.value;
10934        let mut node = ComponentDefinition {
10935            name,
10936            renders: String::new(),
10937            via_shield: String::new(),
10938            on_interact: String::new(),
10939            render_hint: "custom".to_string(),
10940            loc: Loc {
10941                line: tok.line,
10942                column: tok.column,
10943            },
10944            leading_trivia: Vec::new(),
10945            trailing_trivia: Vec::new(),
10946        };
10947        self.consume(TokenType::LBrace)?;
10948        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10949            let field_name = self.current().value.clone();
10950            self.advance();
10951            if !self.check(TokenType::Colon) {
10952                if self.check(TokenType::LBrace) {
10953                    self.skip_braced_block()?;
10954                }
10955                continue;
10956            }
10957            self.advance();
10958            match field_name.as_str() {
10959                "renders" => node.renders = self.consume_any_ident_or_kw()?.value,
10960                "via_shield" => node.via_shield = self.consume_any_ident_or_kw()?.value,
10961                "on_interact" => node.on_interact = self.consume_any_ident_or_kw()?.value,
10962                "render_hint" => {
10963                    let h_tok = self.consume_any_ident_or_kw()?;
10964                    let h = h_tok.value;
10965                    if !matches!(h.as_str(), "card" | "list" | "form" | "chart" | "custom") {
10966                        return Err(ParseError {
10967                            message: format!(
10968                                "Invalid render_hint '{h}' in component '{}' — \
10969                                 expected card | list | form | chart | custom",
10970                                node.name
10971                            ),
10972                            line: h_tok.line,
10973                            column: h_tok.column,
10974                                                    ..Default::default()
10975                        });
10976                    }
10977                    node.render_hint = h;
10978                }
10979                _ => self.skip_value(),
10980            }
10981        }
10982        self.consume(TokenType::RBrace)?;
10983        Ok(node)
10984    }
10985
10986    /// Parse: `view Name { title, components: [...], route }`.
10987    fn parse_view(&mut self) -> Result<ViewDefinition, ParseError> {
10988        let tok = self.consume(TokenType::View)?;
10989        let name = self.consume(TokenType::Identifier)?.value;
10990        let mut node = ViewDefinition {
10991            name,
10992            title: String::new(),
10993            components: Vec::new(),
10994            route: String::new(),
10995            loc: Loc {
10996                line: tok.line,
10997                column: tok.column,
10998            },
10999            leading_trivia: Vec::new(),
11000            trailing_trivia: Vec::new(),
11001        };
11002        self.consume(TokenType::LBrace)?;
11003        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11004            let field_name = self.current().value.clone();
11005            self.advance();
11006            if !self.check(TokenType::Colon) {
11007                if self.check(TokenType::LBrace) {
11008                    self.skip_braced_block()?;
11009                }
11010                continue;
11011            }
11012            self.advance();
11013            match field_name.as_str() {
11014                "title" => node.title = self.consume(TokenType::StringLit)?.value,
11015                "components" => node.components = self.parse_bracketed_identifiers()?,
11016                "route" => node.route = self.consume(TokenType::StringLit)?.value,
11017                _ => self.skip_value(),
11018            }
11019        }
11020        self.consume(TokenType::RBrace)?;
11021        Ok(node)
11022    }
11023
11024    fn parse_axonendpoint(&mut self) -> Result<AxonEndpointDefinition, ParseError> {
11025        let tok = self.consume(TokenType::AxonEndpoint)?;
11026        let name = self.consume(TokenType::Identifier)?.value;
11027        let mut node = AxonEndpointDefinition {
11028            name,
11029            method: String::new(),
11030            path: String::new(),
11031            body_type: String::new(),
11032            execute_flow: String::new(),
11033            output_type: String::new(),
11034            shield_ref: String::new(),
11035            // §Fase 83.a — `cors:` reference; empty ≡ no cors declared
11036            // (D83.5: no CORS headers, ever — secure by default).
11037            cors_ref: String::new(),
11038            retries: None,
11039            timeout: String::new(),
11040            compliance: Vec::new(),
11041            // §Fase 30 — Defaults preserve backwards compat per D1.
11042            transport: "json".to_string(),
11043            keepalive: String::new(),
11044            // §Fase 31.b — Inference fields (parser-default state).
11045            // Both fields toggle/populate only when the source provides
11046            // an explicit `transport:` declaration (parser sets
11047            // `transport_explicit = true`) AND the type-checker walks
11048            // the program to compute `implicit_transport`.
11049            transport_explicit: false,
11050            implicit_transport: String::new(),
11051            // §Fase 32.g (D8) — auth scope; empty list ≡ no auth gate.
11052            requires_capabilities: Vec::new(),
11053            // §Fase 89.a — explicit authorization-coverage opt-out. Default
11054            // false; the §89.b rule requires coverage OR `public: true`.
11055            public: false,
11056            // §Fase 32.h — Replay-token binding (D9 plan-vivo).
11057            // Parser defaults: not explicit; effective value resolved
11058            // at deploy time using the method-default heuristic.
11059            replay_explicit: false,
11060            replay: false,
11061            // §Fase 33.z.k.b (v1.28.0) — Wire-format dialect default
11062            // empty; the runtime classifier resolves the default
11063            // dialect per the algebraic-effect predicate when the
11064            // source omits `transport: sse(<dialect>)`.
11065            transport_dialect: String::new(),
11066            // §Fase 33.z.k.1 (v1.27.1) — Algebraic-effect override.
11067            // Parser default false; populated by the type-checker's
11068            // compute_implicit_transports pass once the full program
11069            // is known (the predicate cross-references tool effects
11070            // declared anywhere in the program).
11071            has_algebraic_stream_effect: false,
11072            // §Fase 36.d (D2) — declared execution backend; empty ≡
11073            // not declared (the endpoint resolves down the Fase 36 D1
11074            // ladder). A non-empty value is validated against the
11075            // closed `AXONENDPOINT_BACKEND_VALUES` catalog below.
11076            backend: String::new(),
11077            // §Fase 37.y (D1) — Path-param names extracted from the
11078            // `path:` string AFTER the field is parsed. Initialized
11079            // empty; populated by `extract_path_param_names` after
11080            // the `path:` field is read in the loop below.
11081            path_params: Vec::new(),
11082            // §Fase 37.y (D2) — Inline `query: { name: Type, name: Type? }`
11083            // block. Initialized empty; populated by the `"query"` arm
11084            // in the field loop below. Closed catalog enforced at parse
11085            // time per `axonendpoint_is_valid_query_param_type`.
11086            query_params: Vec::new(),
11087            loc: Loc {
11088                line: tok.line,
11089                column: tok.column,
11090            },
11091            leading_trivia: Vec::new(),
11092            trailing_trivia: Vec::new(),
11093        };
11094        self.consume(TokenType::LBrace)?;
11095        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11096            let field_name = self.current().value.clone();
11097            self.advance();
11098            if self.check(TokenType::Colon) {
11099                self.advance();
11100                match field_name.as_str() {
11101                    "method" => {
11102                        // §Fase 32.b D3 — closed method enum
11103                        // `{GET, POST, PUT, DELETE, PATCH}`. Unknown
11104                        // values rejected at parse time with smart-
11105                        // suggest hint (Fase 28.e). HEAD/OPTIONS/etc.
11106                        // are runtime-managed and not adopter-
11107                        // declarable.
11108                        let value_tok = self.consume_any_ident_or_kw()?;
11109                        let value_upper = value_tok.value.to_uppercase();
11110                        if !axonendpoint_is_valid_method(&value_upper) {
11111                            let hint = crate::smart_suggest::suggest_for(
11112                                &value_upper,
11113                                AXONENDPOINT_METHOD_VALUES,
11114                            );
11115                            let base = format!(
11116                                "Invalid method '{}' in axonendpoint '{}'.",
11117                                value_tok.value, node.name
11118                            );
11119                            let message = if hint.is_empty() {
11120                                format!(
11121                                    "{base} expected GET | POST | PUT | DELETE | PATCH, found {}",
11122                                    value_tok.value
11123                                )
11124                            } else {
11125                                format!(
11126                                    "{base} {hint} (expected GET | POST | PUT | DELETE | PATCH, found {})",
11127                                    value_tok.value
11128                                )
11129                            };
11130                            return Err(ParseError {
11131                                message,
11132                                line: value_tok.line,
11133                                column: value_tok.column,
11134                                ..Default::default()
11135                            });
11136                        }
11137                        node.method = value_upper;
11138                    }
11139                    "path" => {
11140                        node.path = self.consume(TokenType::StringLit)?.value.clone();
11141                        // §Fase 37.y (D1) — extract `{name}` placeholders
11142                        // for the Request Binding Contract's path-param
11143                        // source. Duplicate `{name}` in the same path
11144                        // is rejected at parse time (HTTP route patterns
11145                        // structurally reject duplicates; surfacing the
11146                        // error here is friendlier than letting axum
11147                        // panic at registration).
11148                        match extract_path_param_names(&node.path) {
11149                            Ok(names) => node.path_params = names,
11150                            Err(dup) => {
11151                                let cur = self.current().clone();
11152                                return Err(ParseError {
11153                                    message: format!(
11154                                        "axonendpoint '{}' declares path '{}' \
11155                                         containing duplicate placeholder '{{{}}}'. \
11156                                         Each `{{name}}` in a `path:` must be \
11157                                         unique — the runtime cannot bind two \
11158                                         path segments to the same name (Fase 37.y D1).",
11159                                        node.name, node.path, dup,
11160                                    ),
11161                                    line: cur.line,
11162                                    column: cur.column,
11163                                    ..Default::default()
11164                                });
11165                            }
11166                        }
11167                    },
11168                    "body" => node.body_type = self.consume_any_ident_or_kw()?.value.clone(),
11169                    "query" => {
11170                        // §Fase 37.y (D2) — Inline query-parameter block.
11171                        // Grammar: `query: { name: Type [, name: Type?]* }`.
11172                        // Closed type catalog
11173                        // `AXONENDPOINT_QUERY_PARAM_TYPES = {Text, Int,
11174                        // Float, Bool, Uuid}`. Optional via `?` suffix
11175                        // reuses `TypeExpr.optional` semantics already in
11176                        // use for flow parameters + body type fields. A
11177                        // duplicate field name in the same block is a
11178                        // parse error (HTTP query strings DO allow
11179                        // multi-value but v1.38.5 binds the first value
11180                        // only — see plan vivo §7 forward-compat).
11181                        //
11182                        // §Fase 37.y (D2 robustness) — declaring `query:`
11183                        // twice on the same axonendpoint silently merged
11184                        // params pre-hardening. Now it's a parse error
11185                        // so an adopter typo / copy-paste mistake
11186                        // surfaces with line + column instead of
11187                        // producing an unexpectedly-augmented endpoint.
11188                        let lbrace_tok = self.consume(TokenType::LBrace)?;
11189                        let block_line = lbrace_tok.line;
11190                        if !node.query_params.is_empty() {
11191                            return Err(ParseError {
11192                                message: format!(
11193                                    "axonendpoint '{}' declares `query: {{ … }}` \
11194                                     more than once. The query-parameter block \
11195                                     is unique per endpoint; combine all params \
11196                                     into a single block (Fase 37.y D2).",
11197                                    node.name,
11198                                ),
11199                                line: lbrace_tok.line,
11200                                column: lbrace_tok.column,
11201                                ..Default::default()
11202                            });
11203                        }
11204                        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11205                            let name_tok = self.consume(TokenType::Identifier)?;
11206                            let field_name = name_tok.value.clone();
11207                            // Duplicate detection within the block.
11208                            if node
11209                                .query_params
11210                                .iter()
11211                                .any(|f| f.name == field_name)
11212                            {
11213                                return Err(ParseError {
11214                                    message: format!(
11215                                        "axonendpoint '{}' declares duplicate \
11216                                         query param '{}' inside `query: {{ … }}`. \
11217                                         Each name must appear at most once \
11218                                         (Fase 37.y D2).",
11219                                        node.name, field_name,
11220                                    ),
11221                                    line: name_tok.line,
11222                                    column: name_tok.column,
11223                                    ..Default::default()
11224                                });
11225                            }
11226                            self.consume(TokenType::Colon)?;
11227                            let type_expr = self.parse_type_expr()?;
11228                            // §Fase 37.y (D2 robustness) — reject generic
11229                            // type expressions on query params. The
11230                            // closed catalog is 5 primitives; container
11231                            // types (`Optional<T>`, `List<T>`, etc.)
11232                            // would mislead the adopter into thinking
11233                            // they bind multi-value query strings
11234                            // (deferred per plan vivo §7) or that
11235                            // `Optional<Text>` is the canonical way to
11236                            // declare an optional query (it's NOT —
11237                            // `Text?` is). Surface the canonical syntax
11238                            // verbatim so the fix is obvious.
11239                            if !type_expr.generic_param.is_empty() {
11240                                let canonical_hint = if type_expr.name == "Optional" {
11241                                    format!(
11242                                        " Use `{}?` (the `?` suffix) for an \
11243                                         optional query param instead of \
11244                                         `Optional<{}>`.",
11245                                        type_expr.generic_param,
11246                                        type_expr.generic_param,
11247                                    )
11248                                } else if type_expr.name == "List" {
11249                                    " Multi-value query params (e.g. `?tag=a&tag=b`) \
11250                                     are honest-deferred from v1.38.5; bind a \
11251                                     single-value `Text` query param and parse \
11252                                     the value inside the flow."
11253                                        .to_string()
11254                                } else {
11255                                    String::new()
11256                                };
11257                                return Err(ParseError {
11258                                    message: format!(
11259                                        "axonendpoint '{}' query param '{}' uses \
11260                                         a generic type `{}<{}>`. Query params \
11261                                         take a primitive type from the closed \
11262                                         catalog ({}); the `?` suffix marks \
11263                                         optional.{} (Fase 37.y D2).",
11264                                        node.name,
11265                                        field_name,
11266                                        type_expr.name,
11267                                        type_expr.generic_param,
11268                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | "),
11269                                        canonical_hint,
11270                                    ),
11271                                    line: type_expr.loc.line,
11272                                    column: type_expr.loc.column,
11273                                    ..Default::default()
11274                                });
11275                            }
11276                            // Validate against the closed catalog. A
11277                            // miss surfaces a Fase 28-style smart-suggest
11278                            // hint when within edit-distance 2.
11279                            if !axonendpoint_is_valid_query_param_type(&type_expr.name) {
11280                                // `smart_suggest::suggest_for` returns
11281                                // pre-formatted prose like
11282                                // "Did you mean `Text`?" or
11283                                // "Did you mean `Text` or `Int`?" (empty
11284                                // when no candidate within edit-distance
11285                                // 2). Concatenate without re-wrapping.
11286                                let hint = crate::smart_suggest::suggest_for(
11287                                    &type_expr.name,
11288                                    AXONENDPOINT_QUERY_PARAM_TYPES,
11289                                );
11290                                let hint_text = if hint.is_empty() {
11291                                    format!(
11292                                        " Expected one of: {}.",
11293                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | ")
11294                                    )
11295                                } else {
11296                                    format!(
11297                                        " {} Expected one of: {}.",
11298                                        hint,
11299                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | ")
11300                                    )
11301                                };
11302                                return Err(ParseError {
11303                                    message: format!(
11304                                        "axonendpoint '{}' query param '{}' has \
11305                                         unsupported type '{}'.{} (Fase 37.y D2).",
11306                                        node.name, field_name, type_expr.name,
11307                                        hint_text,
11308                                    ),
11309                                    line: type_expr.loc.line,
11310                                    column: type_expr.loc.column,
11311                                    ..Default::default()
11312                                });
11313                            }
11314                            node.query_params.push(TypeField {
11315                                name: field_name,
11316                                type_expr,
11317                                loc: Loc {
11318                                    line: name_tok.line,
11319                                    column: name_tok.column,
11320                                },
11321                            });
11322                            // Trailing comma is optional; the next loop
11323                            // iteration handles `}` cleanly. Accept both
11324                            // `name: Type, name: Type` AND `name: Type
11325                            // name: Type` (the existing parser style is
11326                            // forgiving about list separators).
11327                            if self.check(TokenType::Comma) {
11328                                self.advance();
11329                            }
11330                            let _ = block_line; // suppress unused warning
11331                        }
11332                        self.consume(TokenType::RBrace)?;
11333                    },
11334                    "execute" => node.execute_flow = self.consume_any_ident_or_kw()?.value.clone(),
11335                    "output" => {
11336                        // §Fase 38.x.f — promote axonendpoint `output:`
11337                        // parsing from a single token to the full
11338                        // generic-aware type expression (mirroring
11339                        // `parse_step` for FlowStep::Step which already
11340                        // uses `parse_output_type_string`).
11341                        //
11342                        // Pre-38.x.f: `output: List<Item>` captured only
11343                        // `"List"`, dropping `<Item>` (next tokens were
11344                        // either left unconsumed or absorbed by the
11345                        // following field). v1.39.0's narrow cardinality
11346                        // gate happened to fire correctly for `output: T`
11347                        // + retrieve-tail because the singular-detection
11348                        // path used `!starts_with("List<")` — but the
11349                        // SYMMETRIC `output: List<T>` + singular-tail
11350                        // case (38.x.f D3) needs the FULL `List<T>`
11351                        // shape captured; without it the gate sees
11352                        // `"List"` and misclassifies as Singular.
11353                        node.output_type = self.parse_output_type_string()?;
11354                    }
11355                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
11356                    // §Fase 83.a — the `cors: <Name>` reference.
11357                    "cors" => node.cors_ref = self.consume_any_ident_or_kw()?.value.clone(),
11358                    "retries" => node.retries = self.parse_optional_int(),
11359                    "timeout" => {
11360                        let t = self.current().clone();
11361                        self.advance();
11362                        node.timeout = t.value.clone();
11363                    }
11364                    "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
11365                    "replay" => {
11366                        // §Fase 32.h (D9 plan-vivo) — Replay-token binding.
11367                        // Boolean `replay: true | false`. Default (when
11368                        // omitted) is method-derived at deploy-time:
11369                        // POST/PUT → true, GET/DELETE → false. Explicit
11370                        // declaration sets `replay_explicit = true` so
11371                        // the runtime knows NOT to override.
11372                        let value_tok = self.consume(TokenType::Bool)?;
11373                        node.replay = value_tok.value.eq_ignore_ascii_case("true");
11374                        node.replay_explicit = true;
11375                    }
11376                    // §Fase 89.a — `public: true | false`, the explicit
11377                    // authorization-coverage opt-out (doctrine
11378                    // `every_boundary_is_guarded`). Mirrors `replay:`'s bool
11379                    // parse. Default false; the §89.b rule (`axon-T890`)
11380                    // requires a covering discipline OR `public: true`.
11381                    "public" => {
11382                        let value_tok = self.consume(TokenType::Bool)?;
11383                        node.public = value_tok.value.eq_ignore_ascii_case("true");
11384                    }
11385                    "requires" => {
11386                        // §Fase 32.g (D8) — Auth scope per axonendpoint.
11387                        // Closed slug grammar
11388                        // `^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$` enforced
11389                        // at parse time with smart-suggest-style hint.
11390                        // Empty list means "no auth gate" (D9 backwards-
11391                        // compat). Cross-stack with Python parser.
11392                        let bracket_tok = self.current().clone();
11393                        let items = self.parse_bracketed_dot_identifiers()?;
11394                        for slug in &items {
11395                            if !is_valid_capability_slug(slug) {
11396                                return Err(ParseError {
11397                                    message: format!(
11398                                        "Invalid capability slug '{slug}' in axonendpoint '{}' \
11399                                         `requires:`. Capability slugs must match \
11400                                         ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
11401                                         lowercase identifiers starting with a letter. Examples: \
11402                                         `admin`, `legal.read`, `hipaa.phi.read`.",
11403                                        node.name
11404                                    ),
11405                                    line: bracket_tok.line,
11406                                    column: bracket_tok.column,
11407                                    ..Default::default()
11408                                });
11409                            }
11410                        }
11411                        node.requires_capabilities = items;
11412                    }
11413                    // §Fase 30.b — HTTP transport enum (D2 closed) + keepalive (D6 closed).
11414                    // Mirrors `axon/compiler/parser.py` `_parse_axonendpoint`.
11415                    // Drift-gate corpus verifies byte-identical parse cross-stack.
11416                    "transport" => {
11417                        let value_tok = self.consume_any_ident_or_kw()?;
11418                        let value = &value_tok.value;
11419                        if !axonendpoint_is_valid_transport(value) {
11420                            let hint = crate::smart_suggest::suggest_for(
11421                                value,
11422                                AXONENDPOINT_TRANSPORT_VALUES,
11423                            );
11424                            let base = format!(
11425                                "Invalid transport '{}' in axonendpoint '{}'.",
11426                                value, node.name
11427                            );
11428                            let message = if hint.is_empty() {
11429                                format!("{base} expected json | sse | ndjson, found {value}")
11430                            } else {
11431                                format!(
11432                                    "{base} {hint} (expected json | sse | ndjson, found {value})"
11433                                )
11434                            };
11435                            return Err(ParseError {
11436                                message,
11437                                line: value_tok.line,
11438                                column: value_tok.column,
11439                                ..Default::default()
11440                            });
11441                        }
11442                        node.transport = value.clone();
11443                        // §Fase 31.b D1 — mark the field as explicitly
11444                        // declared so the type-checker's implicit-transport
11445                        // inference knows NOT to override this value with
11446                        // the produces_stream-driven inference.
11447                        node.transport_explicit = true;
11448                        // §Fase 33.z.k.b (v1.28.0) — Optional dialect
11449                        // parametrization: `transport: sse(<dialect>)`.
11450                        // Only valid when the base value is `sse`
11451                        // (json + ndjson dialects are the dialects
11452                        // themselves; `json(<x>)` / `ndjson(<x>)`
11453                        // would be parse errors caught below).
11454                        if self.check(TokenType::LParen) {
11455                            if value != "sse" {
11456                                let tok = self.current().clone();
11457                                return Err(ParseError {
11458                                    message: format!(
11459                                        "Dialect parametrization \
11460                                         `transport: {value}(<dialect>)` is \
11461                                         only valid for `sse`; got \
11462                                         `{value}` in axonendpoint '{}'.",
11463                                        node.name
11464                                    ),
11465                                    line: tok.line,
11466                                    column: tok.column,
11467                                    ..Default::default()
11468                                });
11469                            }
11470                            self.advance(); // consume LParen
11471                            let dialect_tok = self.consume_any_ident_or_kw()?;
11472                            let dialect = dialect_tok.value.clone();
11473                            if !AXONENDPOINT_TRANSPORT_DIALECTS
11474                                .iter()
11475                                .any(|&d| d == dialect)
11476                            {
11477                                let hint = crate::smart_suggest::suggest_for(
11478                                    &dialect,
11479                                    AXONENDPOINT_TRANSPORT_DIALECTS,
11480                                );
11481                                let base = format!(
11482                                    "Invalid SSE dialect '{dialect}' in axonendpoint '{}'.",
11483                                    node.name
11484                                );
11485                                let message = if hint.is_empty() {
11486                                    format!(
11487                                        "{base} expected axon | openai | kimi | glm | anthropic, found {dialect}"
11488                                    )
11489                                } else {
11490                                    format!(
11491                                        "{base} {hint} (expected axon | openai | kimi | glm | anthropic, found {dialect})"
11492                                    )
11493                                };
11494                                return Err(ParseError {
11495                                    message,
11496                                    line: dialect_tok.line,
11497                                    column: dialect_tok.column,
11498                                    ..Default::default()
11499                                });
11500                            }
11501                            // Closing RParen.
11502                            let rparen_tok = self.current().clone();
11503                            if !self.check(TokenType::RParen) {
11504                                return Err(ParseError {
11505                                    message: format!(
11506                                        "Expected `)` after dialect name \
11507                                         in axonendpoint '{}' \
11508                                         (transport: sse(<dialect>) grammar).",
11509                                        node.name
11510                                    ),
11511                                    line: rparen_tok.line,
11512                                    column: rparen_tok.column,
11513                                    ..Default::default()
11514                                });
11515                            }
11516                            self.advance(); // consume RParen
11517                            node.transport_dialect = dialect;
11518                        }
11519                    }
11520                    "keepalive" => {
11521                        // Accepts either a DURATION token (e.g. `15s`) or
11522                        // an ident-like token. Validation against the
11523                        // closed enum {5s, 15s, 30s, 60s} happens after.
11524                        let value_tok = self.current().clone();
11525                        self.advance();
11526                        let value = &value_tok.value;
11527                        if !axonendpoint_is_valid_keepalive(value) {
11528                            let hint = crate::smart_suggest::suggest_for(
11529                                value,
11530                                AXONENDPOINT_KEEPALIVE_VALUES,
11531                            );
11532                            let base = format!(
11533                                "Invalid keepalive '{}' in axonendpoint '{}'.",
11534                                value, node.name
11535                            );
11536                            let message = if hint.is_empty() {
11537                                format!("{base} expected 5s | 15s | 30s | 60s, found {value}")
11538                            } else {
11539                                format!(
11540                                    "{base} {hint} (expected 5s | 15s | 30s | 60s, found {value})"
11541                                )
11542                            };
11543                            return Err(ParseError {
11544                                message,
11545                                line: value_tok.line,
11546                                column: value_tok.column,
11547                                ..Default::default()
11548                            });
11549                        }
11550                        node.keepalive = value.clone();
11551                    }
11552                    "backend" => {
11553                        // §Fase 36.d (D2) — declared execution backend.
11554                        // Closed catalog `CANONICAL_PROVIDERS ∪ {auto,
11555                        // stub}`; an unknown name is a parse error with
11556                        // a smart-suggest hint (the same discipline as
11557                        // `method`/`transport`/`keepalive`). The
11558                        // type-checker re-validates defensively for
11559                        // ASTs built outside the parser (LSP, tests).
11560                        let value_tok = self.consume_any_ident_or_kw()?;
11561                        let value = &value_tok.value;
11562                        if !axonendpoint_is_valid_backend(value) {
11563                            let hint = crate::smart_suggest::suggest_for(
11564                                value,
11565                                AXONENDPOINT_BACKEND_VALUES,
11566                            );
11567                            let expected = AXONENDPOINT_BACKEND_VALUES.join(" | ");
11568                            let base = format!(
11569                                "Invalid backend '{}' in axonendpoint '{}'.",
11570                                value, node.name
11571                            );
11572                            let message = if hint.is_empty() {
11573                                format!("{base} expected {expected}, found {value}")
11574                            } else {
11575                                format!(
11576                                    "{base} {hint} (expected {expected}, found {value})"
11577                                )
11578                            };
11579                            return Err(ParseError {
11580                                message,
11581                                line: value_tok.line,
11582                                column: value_tok.column,
11583                                ..Default::default()
11584                            });
11585                        }
11586                        node.backend = value.clone();
11587                    }
11588                    _ => self.skip_value(),
11589                }
11590            } else if self.check(TokenType::LBrace) {
11591                self.skip_braced_block()?;
11592            }
11593        }
11594        self.consume(TokenType::RBrace)?;
11595        Ok(node)
11596    }
11597
11598    // ── Numeric helpers for Tier 2 field parsing ────────────────────
11599
11600    fn parse_optional_int(&mut self) -> Option<i64> {
11601        let tok = self.current().clone();
11602        match tok.ttype {
11603            TokenType::Integer => {
11604                self.advance();
11605                tok.value.parse::<i64>().ok()
11606            }
11607            _ => {
11608                self.advance();
11609                None
11610            }
11611        }
11612    }
11613
11614    fn parse_optional_float(&mut self) -> Option<f64> {
11615        let tok = self.current().clone();
11616        match tok.ttype {
11617            TokenType::Float | TokenType::Integer => {
11618                self.advance();
11619                tok.value.parse::<f64>().ok()
11620            }
11621            _ => {
11622                self.advance();
11623                None
11624            }
11625        }
11626    }
11627
11628    // ── LAMBDA DATA (ΛD) ──────────────────────────────────────────
11629
11630    fn parse_lambda_data(&mut self) -> Result<LambdaDataDefinition, ParseError> {
11631        let tok = self.consume(TokenType::Lambda)?;
11632        let name = self.consume(TokenType::Identifier)?;
11633        self.consume(TokenType::LBrace)?;
11634
11635        let mut node = LambdaDataDefinition {
11636            name: name.value.clone(),
11637            ontology: String::new(),
11638            certainty: 1.0,
11639            temporal_frame_start: String::new(),
11640            temporal_frame_end: String::new(),
11641            provenance: String::new(),
11642            derivation: String::new(),
11643            loc: Loc {
11644                line: tok.line,
11645                column: tok.column,
11646            },
11647            leading_trivia: Vec::new(),
11648            trailing_trivia: Vec::new(),
11649        };
11650
11651        while !self.check(TokenType::RBrace) {
11652            let field = self.current().clone();
11653            match field.ttype {
11654                TokenType::Ontology => {
11655                    self.advance();
11656                    self.consume(TokenType::Colon)?;
11657                    node.ontology = self.consume(TokenType::StringLit)?.value.clone();
11658                }
11659                TokenType::Certainty => {
11660                    self.advance();
11661                    self.consume(TokenType::Colon)?;
11662                    let val = self.current().clone();
11663                    match val.ttype {
11664                        TokenType::Float => {
11665                            self.advance();
11666                            node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
11667                        }
11668                        TokenType::Integer => {
11669                            self.advance();
11670                            node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
11671                        }
11672                        _ => {
11673                            return Err(ParseError {
11674                                message: format!(
11675                                    "Expected number for certainty, got '{}'",
11676                                    val.value
11677                                ),
11678                                line: val.line,
11679                                column: val.column,
11680                                                            ..Default::default()
11681                            });
11682                        }
11683                    }
11684                }
11685                TokenType::TemporalFrame => {
11686                    self.advance();
11687                    self.consume(TokenType::Colon)?;
11688                    node.temporal_frame_start = self.consume(TokenType::StringLit)?.value.clone();
11689                    // Optional second string for end frame
11690                    if self.check(TokenType::StringLit) {
11691                        node.temporal_frame_end = self.consume(TokenType::StringLit)?.value.clone();
11692                    }
11693                }
11694                TokenType::Provenance => {
11695                    self.advance();
11696                    self.consume(TokenType::Colon)?;
11697                    node.provenance = self.consume(TokenType::StringLit)?.value.clone();
11698                }
11699                TokenType::Derivation => {
11700                    self.advance();
11701                    self.consume(TokenType::Colon)?;
11702                    let d = self.current().clone();
11703                    self.advance();
11704                    node.derivation = d.value.clone();
11705                }
11706                _ => {
11707                    // Skip unknown fields gracefully
11708                    self.advance();
11709                    if self.check(TokenType::Colon) {
11710                        self.advance();
11711                        self.skip_value();
11712                    }
11713                }
11714            }
11715        }
11716
11717        self.consume(TokenType::RBrace)?;
11718        Ok(node)
11719    }
11720
11721    fn parse_lambda_data_apply(&mut self) -> Result<LambdaDataApplyNode, ParseError> {
11722        let tok = self.consume(TokenType::Lambda)?;
11723        let lambda_name = self.consume(TokenType::Identifier)?;
11724
11725        // Expect "on" keyword (parsed as identifier since it's not reserved)
11726        let on_tok = self.current().clone();
11727        self.advance();
11728        if on_tok.value != "on" {
11729            return Err(ParseError {
11730                message: format!(
11731                    "Expected 'on' after lambda data name in flow step, got '{}'",
11732                    on_tok.value
11733                ),
11734                line: on_tok.line,
11735                column: on_tok.column,
11736                            ..Default::default()
11737            });
11738        }
11739
11740        let target = self.current().clone();
11741        self.advance();
11742
11743        let mut output_type = String::new();
11744        if self.check(TokenType::Arrow) {
11745            self.advance();
11746            output_type = self.consume(TokenType::Identifier)?.value.clone();
11747        }
11748
11749        Ok(LambdaDataApplyNode {
11750            lambda_data_name: lambda_name.value.clone(),
11751            target: target.value.clone(),
11752            output_type,
11753            loc: Loc {
11754                line: tok.line,
11755                column: tok.column,
11756            },
11757        })
11758    }
11759
11760    // ── GENERIC (Tier 2+) ────────────────────────────────────────
11761
11762    fn parse_generic_declaration(&mut self) -> Result<Declaration, ParseError> {
11763        let kw_tok = self.current().clone();
11764        self.advance(); // consume keyword
11765
11766        // Try to consume a name (identifier or keyword-as-name)
11767        let name = if self.current().ttype == TokenType::Identifier {
11768            let n = self.current().value.clone();
11769            self.advance();
11770            n
11771        } else if !self.check(TokenType::LBrace)
11772            && !self.check(TokenType::LParen)
11773            && !self.check(TokenType::Eof)
11774            && self
11775                .current()
11776                .value
11777                .chars()
11778                .all(|c| c.is_alphanumeric() || c == '_')
11779        {
11780            let n = self.current().value.clone();
11781            self.advance();
11782            n
11783        } else {
11784            String::new()
11785        };
11786
11787        // Skip optional parens: (...)
11788        if self.check(TokenType::LParen) {
11789            self.advance();
11790            let mut depth = 1u32;
11791            while depth > 0 && !self.check(TokenType::Eof) {
11792                if self.check(TokenType::LParen) {
11793                    depth += 1;
11794                } else if self.check(TokenType::RParen) {
11795                    depth -= 1;
11796                }
11797                self.advance();
11798            }
11799        }
11800
11801        // Skip tokens until LBrace or next declaration
11802        while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
11803            if self.check(TokenType::Eof) {
11804                break;
11805            }
11806            self.advance();
11807        }
11808
11809        // Skip braced block if present
11810        if self.check(TokenType::LBrace) {
11811            self.skip_braced_block()?;
11812        }
11813
11814        Ok(Declaration::Generic(GenericDeclaration {
11815            keyword: kw_tok.value,
11816            name,
11817            loc: Loc {
11818                line: kw_tok.line,
11819                column: kw_tok.column,
11820            },
11821            leading_trivia: Vec::new(),
11822            trailing_trivia: Vec::new(),
11823        }))
11824    }
11825
11826    // ──────────────────────────────────────────────────────────────────
11827    //  §λ-L-E Fase 13 — Mobile Typed Channels parsers
11828    //  (paper_mobile_channels.md §3 + plan/fase_13)
11829    //  Direct port of axon/compiler/parser.py:_parse_channel/emit/publish/discover.
11830    // ──────────────────────────────────────────────────────────────────
11831
11832    /// Parse: `channel Name { message, qos, lifetime, persistence, shield }`.
11833    fn parse_channel(&mut self) -> Result<ChannelDefinition, ParseError> {
11834        let tok = self.consume(TokenType::Channel)?;
11835        let name = self.consume(TokenType::Identifier)?.value;
11836        let mut node = ChannelDefinition {
11837            name: name.clone(),
11838            message: String::new(),
11839            qos: "at_least_once".to_string(),
11840            lifetime: "affine".to_string(),
11841            persistence: "ephemeral".to_string(),
11842            shield_ref: String::new(),
11843            loc: Loc {
11844                line: tok.line,
11845                column: tok.column,
11846            },
11847            leading_trivia: Vec::new(),
11848            trailing_trivia: Vec::new(),
11849        };
11850        self.consume(TokenType::LBrace)?;
11851        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11852            let field_tok = self.current().clone();
11853            let field_name = field_tok.value.clone();
11854            self.advance();
11855            if !self.check(TokenType::Colon) {
11856                if self.check(TokenType::LBrace) {
11857                    self.skip_braced_block()?;
11858                }
11859                continue;
11860            }
11861            self.advance();
11862            match field_name.as_str() {
11863                "message" => node.message = self.parse_channel_message_type()?,
11864                "qos" => {
11865                    let q_tok = self.consume_any_ident_or_kw()?;
11866                    if !matches!(
11867                        q_tok.value.as_str(),
11868                        "at_most_once" | "at_least_once" | "exactly_once" | "broadcast" | "queue"
11869                    ) {
11870                        return Err(ParseError {
11871                            message: format!(
11872                                "Invalid qos '{}' in channel '{}' — \
11873                                 expected at_most_once | at_least_once | \
11874                                 exactly_once | broadcast | queue",
11875                                q_tok.value, name
11876                            ),
11877                            line: q_tok.line,
11878                            column: q_tok.column,
11879                                                    ..Default::default()
11880                        });
11881                    }
11882                    node.qos = q_tok.value;
11883                }
11884                "lifetime" => {
11885                    let lt_tok = self.consume_any_ident_or_kw()?;
11886                    if !matches!(lt_tok.value.as_str(), "linear" | "affine" | "persistent") {
11887                        return Err(ParseError {
11888                            message: format!(
11889                                "Invalid lifetime '{}' in channel '{}' — \
11890                                 expected linear | affine | persistent",
11891                                lt_tok.value, name
11892                            ),
11893                            line: lt_tok.line,
11894                            column: lt_tok.column,
11895                                                    ..Default::default()
11896                        });
11897                    }
11898                    node.lifetime = lt_tok.value;
11899                }
11900                "persistence" => {
11901                    let p_tok = self.consume_any_ident_or_kw()?;
11902                    if !matches!(p_tok.value.as_str(), "ephemeral" | "persistent_axonstore") {
11903                        return Err(ParseError {
11904                            message: format!(
11905                                "Invalid persistence '{}' in channel '{}' — \
11906                                 expected ephemeral | persistent_axonstore",
11907                                p_tok.value, name
11908                            ),
11909                            line: p_tok.line,
11910                            column: p_tok.column,
11911                                                    ..Default::default()
11912                        });
11913                    }
11914                    node.persistence = p_tok.value;
11915                }
11916                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
11917                _ => self.skip_value(),
11918            }
11919        }
11920        self.consume(TokenType::RBrace)?;
11921        Ok(node)
11922    }
11923
11924    /// Parse a `message:` value, supporting nested `Channel<…>`
11925    /// (second-order session types — paper §3.3).
11926    fn parse_channel_message_type(&mut self) -> Result<String, ParseError> {
11927        let head = self.consume(TokenType::Identifier)?;
11928        let mut spelling = head.value;
11929        if self.check(TokenType::Lt) {
11930            self.advance();
11931            let inner = self.parse_channel_message_type()?;
11932            self.consume(TokenType::Gt)?;
11933            spelling = format!("{}<{}>", spelling, inner);
11934        }
11935        Ok(spelling)
11936    }
11937
11938    /// Parse: `emit ChannelName(value_ref)` — Chan-Output / Chan-Mobility.
11939    ///
11940    /// `value_ref` accepts a bare identifier (variable / channel name for
11941    /// mobility) or a dotted path (`Step.output.field`) referencing a prior
11942    /// step result (Fase 13.i — runtime resolves via ContextManager).
11943    fn parse_emit_step(&mut self) -> Result<FlowStep, ParseError> {
11944        let tok = self.consume(TokenType::Emit)?;
11945        let channel = self.consume(TokenType::Identifier)?.value;
11946        self.consume(TokenType::LParen)?;
11947        let value = self.parse_emit_value_ref()?;
11948        self.consume(TokenType::RParen)?;
11949        Ok(FlowStep::Emit(EmitStatement {
11950            channel_ref: channel,
11951            value_ref: value,
11952            loc: Loc {
11953                line: tok.line,
11954                column: tok.column,
11955            },
11956        }))
11957    }
11958
11959    /// §Fase 92.b — parse `mint <Credential> as <binding>`. The credential
11960    /// reference must resolve to a declared `credential` (`axon-T895`,
11961    /// type-checker); the binding is a fresh flow-scoped name receiving the
11962    /// raw bearer string. Both tokens are required — a `mint` with no
11963    /// binding would mint authority into the void.
11964    fn parse_mint_step(&mut self) -> Result<FlowStep, ParseError> {
11965        let tok = self.consume(TokenType::Mint)?;
11966        let credential_ref = self.consume(TokenType::Identifier)?.value;
11967        self.consume(TokenType::As)?;
11968        let binding = self.consume(TokenType::Identifier)?.value;
11969        Ok(FlowStep::Mint(MintStep {
11970            credential_ref,
11971            binding,
11972            loc: Loc {
11973                line: tok.line,
11974                column: tok.column,
11975            },
11976        }))
11977    }
11978
11979    /// §Fase 94.b — parse `rotate <SecretsStore> [where "<filter>"] with
11980    /// <Tool> as <binding>` (doctrine `rotation_without_revelation`).
11981    ///
11982    /// All three anchors are grammar, not convention: the store names WHAT
11983    /// may rotate (a `backend: secrets` class view — `axon-T898` in the
11984    /// type-checker), the tool names WHO performs the exchange
11985    /// (`axon-T899`), and the binding receives the metadata-only summary —
11986    /// a `rotate` without a binding would renew authority with no
11987    /// observable outcome, so `as` is REQUIRED (the `mint` posture). The
11988    /// `where` filter is optional (§67 string grammar, proven against the
11989    /// synthesized metadata schema); omitting it rotates the WHOLE class —
11990    /// the deliberate post-breach bulk shape. `with` is a soft keyword
11991    /// (not a lexer token): reserving it globally would break every
11992    /// adopter identifier named `with`.
11993    fn parse_rotate_step(&mut self) -> Result<FlowStep, ParseError> {
11994        let tok = self.consume(TokenType::Rotate)?;
11995        let store_ref = self.consume(TokenType::Identifier)?.value;
11996        let mut where_expr = String::new();
11997        if self.check(TokenType::Where) {
11998            self.advance();
11999            where_expr = self.consume(TokenType::StringLit)?.value.clone();
12000        }
12001        let with_tok = self.current().clone();
12002        if with_tok.value != "with" {
12003            return Err(ParseError {
12004                message: format!(
12005                    "Expected `with <Tool>` after `rotate {store_ref}{}`, found '{}'. \
12006                     A rotation names the tool that performs the renewal exchange: \
12007                     `rotate {store_ref} [where \"<filter>\"] with <Tool> as <binding>`.",
12008                    if where_expr.is_empty() { "" } else { " where …" },
12009                    with_tok.value
12010                ),
12011                line: with_tok.line,
12012                column: with_tok.column,
12013                ..Default::default()
12014            });
12015        }
12016        self.advance();
12017        let tool_ref = self.consume(TokenType::Identifier)?.value;
12018        self.consume(TokenType::As)?;
12019        let binding = self.consume(TokenType::Identifier)?.value;
12020        Ok(FlowStep::Rotate(RotateStep {
12021            store_ref,
12022            where_expr,
12023            tool_ref,
12024            binding,
12025            loc: Loc {
12026                line: tok.line,
12027                column: tok.column,
12028            },
12029        }))
12030    }
12031
12032    /// Parse: `IDENTIFIER ('.' (IDENTIFIER | keyword))*` → dot-joined string
12033    /// (Fase 13.i).
12034    ///
12035    /// Mirrors the Python `_parse_emit_value_ref` helper exactly so the IR
12036    /// JSON for `emit Hello(Build.output)` is byte-identical between the
12037    /// two reference implementations.
12038    ///
12039    /// The HEAD must be a real ``Identifier``. Subsequent segments after a
12040    /// `.` may be identifiers OR keywords — common field names like
12041    /// ``output``, ``result``, ``message``, ``state``, etc. are reserved
12042    /// words in Axon but adopters must be able to write them as
12043    /// dotted-access segments. The accepting predicate:
12044    ///   - the lexer carried a non-empty `value` (every Word-like token does)
12045    ///   - the value's first byte is a letter or underscore (filters out
12046    ///     punctuation tokens such as ',', '{', etc.)
12047    fn parse_emit_value_ref(&mut self) -> Result<String, ParseError> {
12048        let head = self.consume(TokenType::Identifier)?.value;
12049        let mut parts = vec![head];
12050        while self.check(TokenType::Dot) {
12051            self.advance(); // consume '.'
12052            let next_tok = self.current().clone();
12053            let valid = !next_tok.value.is_empty()
12054                && next_tok.value.as_bytes()[0].is_ascii_alphabetic()
12055                || next_tok.value.starts_with('_');
12056            if !valid {
12057                return Err(ParseError {
12058                    message: format!(
12059                        "Expected identifier or keyword after '.' in dotted \
12060                         access, found {:?}",
12061                        next_tok.value
12062                    ),
12063                    line: next_tok.line,
12064                    column: next_tok.column,
12065                                    ..Default::default()
12066                });
12067            }
12068            self.advance();
12069            parts.push(next_tok.value);
12070        }
12071        Ok(parts.join("."))
12072    }
12073
12074    /// Parse: `publish ChannelName within ShieldName` — Publish-Ext (D8).
12075    fn parse_publish_step(&mut self) -> Result<FlowStep, ParseError> {
12076        let tok = self.consume(TokenType::Publish)?;
12077        let channel = self.consume(TokenType::Identifier)?.value;
12078        self.consume(TokenType::Within)?;
12079        let shield = self.consume(TokenType::Identifier)?.value;
12080        Ok(FlowStep::Publish(PublishStatement {
12081            channel_ref: channel,
12082            shield_ref: shield,
12083            loc: Loc {
12084                line: tok.line,
12085                column: tok.column,
12086            },
12087        }))
12088    }
12089
12090    /// Parse: `discover ChannelName as alias` — dual of publish.
12091    fn parse_discover_step(&mut self) -> Result<FlowStep, ParseError> {
12092        let tok = self.consume(TokenType::Discover)?;
12093        let cap = self.consume(TokenType::Identifier)?.value;
12094        self.consume(TokenType::As)?;
12095        let alias = self.consume(TokenType::Identifier)?.value;
12096        Ok(FlowStep::Discover(DiscoverStatement {
12097            capability_ref: cap,
12098            alias,
12099            loc: Loc {
12100                line: tok.line,
12101                column: tok.column,
12102            },
12103        }))
12104    }
12105}
12106
12107// ── §λ-L-E Fase 13 — Mobile Typed Channels parser tests ─────────────────────
12108
12109#[cfg(test)]
12110mod fase13_parser_tests {
12111    use super::*;
12112    use crate::lexer::Lexer;
12113
12114    fn parse(src: &str) -> Result<Program, ParseError> {
12115        let tokens = Lexer::new(src, "<test>").tokenize().expect("lex");
12116        Parser::new(tokens).parse()
12117    }
12118
12119    #[test]
12120    fn channel_full_parses() {
12121        let src = r#"channel C { message: Order qos: at_least_once lifetime: affine persistence: ephemeral shield: Gate }"#;
12122        let prog = parse(src).expect("parse");
12123        match &prog.declarations[0] {
12124            Declaration::Channel(c) => {
12125                assert_eq!(c.name, "C");
12126                assert_eq!(c.message, "Order");
12127                assert_eq!(c.qos, "at_least_once");
12128                assert_eq!(c.lifetime, "affine");
12129                assert_eq!(c.persistence, "ephemeral");
12130                assert_eq!(c.shield_ref, "Gate");
12131            }
12132            _ => panic!("expected ChannelDefinition"),
12133        }
12134    }
12135
12136    #[test]
12137    fn channel_defaults_match_paper_d1() {
12138        let prog = parse("channel C { message: Order }").expect("parse");
12139        if let Declaration::Channel(c) = &prog.declarations[0] {
12140            assert_eq!(c.qos, "at_least_once"); // default
12141            assert_eq!(c.lifetime, "affine"); // D1 default
12142            assert_eq!(c.persistence, "ephemeral");
12143            assert_eq!(c.shield_ref, "");
12144        } else {
12145            panic!("expected ChannelDefinition");
12146        }
12147    }
12148
12149    #[test]
12150    fn channel_second_order_message_type_parses() {
12151        let prog = parse("channel C { message: Channel<Order> }").expect("parse");
12152        if let Declaration::Channel(c) = &prog.declarations[0] {
12153            assert_eq!(c.message, "Channel<Order>");
12154        } else {
12155            panic!("expected ChannelDefinition");
12156        }
12157    }
12158
12159    #[test]
12160    fn channel_nested_channel_message_type_parses() {
12161        let prog = parse("channel C { message: Channel<Channel<Order>> }").expect("parse");
12162        if let Declaration::Channel(c) = &prog.declarations[0] {
12163            assert_eq!(c.message, "Channel<Channel<Order>>");
12164        } else {
12165            panic!("expected ChannelDefinition");
12166        }
12167    }
12168
12169    #[test]
12170    fn channel_invalid_qos_rejected() {
12171        let err = parse("channel C { message: T qos: bogus }").unwrap_err();
12172        assert!(err.message.contains("Invalid qos"), "got {}", err.message);
12173    }
12174
12175    #[test]
12176    fn channel_invalid_lifetime_rejected() {
12177        let err = parse("channel C { message: T lifetime: eternal }").unwrap_err();
12178        assert!(
12179            err.message.contains("Invalid lifetime"),
12180            "got {}",
12181            err.message
12182        );
12183    }
12184
12185    #[test]
12186    fn channel_invalid_persistence_rejected() {
12187        let err = parse("channel C { message: T persistence: forever }").unwrap_err();
12188        assert!(
12189            err.message.contains("Invalid persistence"),
12190            "got {}",
12191            err.message
12192        );
12193    }
12194
12195    #[test]
12196    fn emit_value_parses() {
12197        let src = "flow f() -> Out { emit C(payload) }";
12198        let prog = parse(src).expect("parse");
12199        if let Declaration::Flow(f) = &prog.declarations[0] {
12200            match &f.body[0] {
12201                FlowStep::Emit(e) => {
12202                    assert_eq!(e.channel_ref, "C");
12203                    assert_eq!(e.value_ref, "payload");
12204                }
12205                other => panic!("expected Emit, got {:?}", other),
12206            }
12207        } else {
12208            panic!("expected Flow");
12209        }
12210    }
12211
12212    #[test]
12213    fn publish_within_shield_parses() {
12214        let src = "flow f() -> Cap { publish C within Gate }";
12215        let prog = parse(src).expect("parse");
12216        if let Declaration::Flow(f) = &prog.declarations[0] {
12217            match &f.body[0] {
12218                FlowStep::Publish(p) => {
12219                    assert_eq!(p.channel_ref, "C");
12220                    assert_eq!(p.shield_ref, "Gate");
12221                }
12222                other => panic!("expected Publish, got {:?}", other),
12223            }
12224        } else {
12225            panic!("expected Flow");
12226        }
12227    }
12228
12229    #[test]
12230    fn discover_with_alias_parses() {
12231        let src = "flow f() -> Out { discover C as ch }";
12232        let prog = parse(src).expect("parse");
12233        if let Declaration::Flow(f) = &prog.declarations[0] {
12234            match &f.body[0] {
12235                FlowStep::Discover(d) => {
12236                    assert_eq!(d.capability_ref, "C");
12237                    assert_eq!(d.alias, "ch");
12238                }
12239                other => panic!("expected Discover, got {:?}", other),
12240            }
12241        } else {
12242            panic!("expected Flow");
12243        }
12244    }
12245
12246    #[test]
12247    fn listen_typed_ref_sets_flag_true() {
12248        let src = "daemon D() { goal: \"x\" listen C as ev { } }";
12249        let prog = parse(src).expect("parse");
12250        if let Declaration::Daemon(d) = &prog.declarations[0] {
12251            assert_eq!(d.listeners.len(), 1);
12252            assert_eq!(d.listeners[0].channel, "C");
12253            assert!(d.listeners[0].channel_is_ref, "typed ref ⇒ true");
12254        } else {
12255            panic!("expected Daemon");
12256        }
12257    }
12258
12259    #[test]
12260    fn listen_string_topic_legacy_flag_false() {
12261        let src = "daemon D() { goal: \"x\" listen \"orders\" as ev { } }";
12262        let prog = parse(src).expect("parse");
12263        if let Declaration::Daemon(d) = &prog.declarations[0] {
12264            assert_eq!(d.listeners.len(), 1);
12265            assert_eq!(d.listeners[0].channel, "orders");
12266            assert!(!d.listeners[0].channel_is_ref, "string topic ⇒ false");
12267        } else {
12268            panic!("expected Daemon");
12269        }
12270    }
12271
12272    // ── Fase 13.i — emit value_ref accepts dotted access ───────────
12273
12274    fn extract_first_emit(prog: &Program) -> &EmitStatement {
12275        if let Declaration::Flow(f) = &prog.declarations[0] {
12276            if let FlowStep::Emit(e) = &f.body[0] {
12277                return e;
12278            }
12279        }
12280        panic!("expected emit statement at flow body[0]");
12281    }
12282
12283    #[test]
12284    fn emit_accepts_bare_identifier_value_ref() {
12285        // Pre-13.i baseline — must keep working.
12286        let prog = parse("flow f() -> Out { emit Hello(payload) }").expect("parse");
12287        let emit = extract_first_emit(&prog);
12288        assert_eq!(emit.channel_ref, "Hello");
12289        assert_eq!(emit.value_ref, "payload");
12290    }
12291
12292    #[test]
12293    fn emit_accepts_two_segment_dotted_value_ref() {
12294        // The exact case adopters reported as broken before 13.i.
12295        let prog = parse("flow f() -> Out { emit Hello(Build.output) }").expect("parse");
12296        let emit = extract_first_emit(&prog);
12297        assert_eq!(emit.value_ref, "Build.output");
12298    }
12299
12300    #[test]
12301    fn emit_accepts_three_segment_nested_dotted_value_ref() {
12302        let prog = parse("flow f() -> Out { emit Score(Analyze.result.score) }").expect("parse");
12303        let emit = extract_first_emit(&prog);
12304        assert_eq!(emit.value_ref, "Analyze.result.score");
12305    }
12306
12307    #[test]
12308    fn emit_dotted_with_trailing_dot_fails() {
12309        // Trailing `.` must still error — every '.' demands an identifier.
12310        let result = parse("flow f() -> Out { emit Hello(Build.) }");
12311        assert!(result.is_err(), "expected parse error for trailing dot");
12312    }
12313}
12314
12315// ── §Fase 14.a — declaration_trivia parallel channel tests ──────────────────
12316
12317#[cfg(test)]
12318mod fase14a_declaration_trivia_tests {
12319    use super::*;
12320    use crate::lexer::Lexer;
12321    use crate::tokens::TriviaKind;
12322
12323    fn parse(src: &str) -> Program {
12324        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
12325        Parser::new(toks).parse().expect("parse")
12326    }
12327
12328    #[test]
12329    fn no_comments_means_empty_trivia_per_decl() {
12330        let prog = parse("flow F() -> Out { }");
12331        assert_eq!(prog.declarations.len(), 1);
12332        assert_eq!(prog.declaration_trivia.len(), 1);
12333        assert!(prog.declaration_trivia[0].leading.is_empty());
12334        assert!(prog.declaration_trivia[0].trailing.is_empty());
12335    }
12336
12337    #[test]
12338    fn doc_line_comment_attaches_as_leading() {
12339        let prog = parse("/// Documents F\nflow F() -> Out { }");
12340        let triv = &prog.declaration_trivia[0];
12341        assert_eq!(triv.leading.len(), 1);
12342        assert_eq!(triv.leading[0].kind, TriviaKind::DocLine);
12343        assert!(triv.leading[0].is_doc());
12344        assert_eq!(triv.leading[0].text, "/// Documents F");
12345    }
12346
12347    #[test]
12348    fn regular_line_comment_attaches_as_leading() {
12349        let prog = parse("// header\nflow F() -> Out { }");
12350        let triv = &prog.declaration_trivia[0];
12351        assert_eq!(triv.leading.len(), 1);
12352        assert_eq!(triv.leading[0].kind, TriviaKind::Line);
12353        assert!(!triv.leading[0].is_doc());
12354    }
12355
12356    #[test]
12357    fn block_doc_comment_attaches_as_leading() {
12358        let prog = parse("/** Doc block */\nflow F() -> Out { }");
12359        let triv = &prog.declaration_trivia[0];
12360        assert_eq!(triv.leading[0].kind, TriviaKind::DocBlock);
12361        assert!(triv.leading[0].is_doc());
12362    }
12363
12364    #[test]
12365    fn multiple_comments_collected_in_source_order() {
12366        let src = "/// First\n/// Second\nflow F() -> Out { }";
12367        let prog = parse(src);
12368        let triv = &prog.declaration_trivia[0];
12369        assert_eq!(triv.leading.len(), 2);
12370        assert_eq!(triv.leading[0].text, "/// First");
12371        assert_eq!(triv.leading[1].text, "/// Second");
12372    }
12373
12374    #[test]
12375    fn three_decls_each_get_own_leading() {
12376        let src = "/// for A\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }\n/// for C\nflow C() -> Out { }";
12377        let prog = parse(src);
12378        assert_eq!(prog.declarations.len(), 3);
12379        assert_eq!(prog.declaration_trivia.len(), 3);
12380        for (idx, name) in ["A", "B", "C"].iter().enumerate() {
12381            let triv = &prog.declaration_trivia[idx];
12382            assert_eq!(triv.leading.len(), 1);
12383            assert_eq!(triv.leading[0].text, format!("/// for {name}"));
12384        }
12385    }
12386
12387    #[test]
12388    fn trailing_comment_attaches_to_last_token_of_decl() {
12389        // Comment on the same line as the decl's closing brace.
12390        let prog = parse("flow F() -> Out { } // tail");
12391        let triv = &prog.declaration_trivia[0];
12392        assert_eq!(triv.trailing.len(), 1);
12393        assert_eq!(triv.trailing[0].text, "// tail");
12394    }
12395
12396    #[test]
12397    fn mixed_doc_and_regular_preserve_order_between_decls() {
12398        let src = "/// doc for A\nflow A() -> Out { }\n\n// header line\n/// doc for B\nflow B() -> Out { }";
12399        let prog = parse(src);
12400        assert_eq!(prog.declarations.len(), 2);
12401        // A: just the doc comment.
12402        assert_eq!(prog.declaration_trivia[0].leading.len(), 1);
12403        // B: header + doc, in source order.
12404        assert_eq!(prog.declaration_trivia[1].leading.len(), 2);
12405        assert_eq!(prog.declaration_trivia[1].leading[0].text, "// header line");
12406        assert_eq!(prog.declaration_trivia[1].leading[1].text, "/// doc for B");
12407    }
12408
12409    #[test]
12410    fn parser_unaffected_by_comments_in_grammar_path() {
12411        // The parser must accept comments interleaved between every
12412        // legal token without affecting the AST shape it produces.
12413        // This is the regression guard for "lossless lexing must not
12414        // change parsing semantics."
12415        let src =
12416            "// before flow\nflow /* between flow and name */ F() -> Out {\n  // body comment\n}";
12417        let prog = parse(src);
12418        assert_eq!(prog.declarations.len(), 1);
12419        if let Declaration::Flow(f) = &prog.declarations[0] {
12420            assert_eq!(f.name, "F");
12421        } else {
12422            panic!("expected Flow declaration");
12423        }
12424    }
12425}
12426
12427// ── §Fase 14.b — per-struct trivia fields tests ─────────────────────────────
12428//
12429// 14.b spreads `leading_trivia` / `trailing_trivia` into every Declaration
12430// variant struct (FlowDefinition, ChannelDefinition, PersonaDefinition, …).
12431// The Python AST already had this shape since 14.a; 14.b achieves Rust
12432// parity. The side-channel `Program.declaration_trivia` is preserved for
12433// backward compat — these tests verify the new direct access path.
12434
12435#[cfg(test)]
12436mod fase14b_per_struct_trivia_tests {
12437    use super::*;
12438    use crate::lexer::Lexer;
12439    use crate::tokens::TriviaKind;
12440
12441    fn parse(src: &str) -> Program {
12442        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
12443        Parser::new(toks).parse().expect("parse")
12444    }
12445
12446    #[test]
12447    fn flow_definition_carries_leading_trivia_directly() {
12448        let prog = parse("/// documents F\nflow F() -> Out { }");
12449        if let Declaration::Flow(f) = &prog.declarations[0] {
12450            assert_eq!(f.leading_trivia.len(), 1);
12451            assert_eq!(f.leading_trivia[0].kind, TriviaKind::DocLine);
12452            assert_eq!(f.leading_trivia[0].text, "/// documents F");
12453            assert!(f.trailing_trivia.is_empty());
12454        } else {
12455            panic!("expected Flow declaration");
12456        }
12457    }
12458
12459    #[test]
12460    fn flow_definition_carries_trailing_trivia_directly() {
12461        let prog = parse("flow F() -> Out { } // tail comment");
12462        if let Declaration::Flow(f) = &prog.declarations[0] {
12463            assert_eq!(f.trailing_trivia.len(), 1);
12464            assert_eq!(f.trailing_trivia[0].text, "// tail comment");
12465        } else {
12466            panic!("expected Flow declaration");
12467        }
12468    }
12469
12470    #[test]
12471    fn channel_definition_carries_trivia_directly() {
12472        // ChannelDefinition is a Tier-1 declaration; verify per-struct fields
12473        // populate just like FlowDefinition.
12474        let src = concat!(
12475            "/// inbound order events\n",
12476            "channel Orders {\n",
12477            "    message:     Order\n",
12478            "    qos:         at_least_once\n",
12479            "    lifetime:    affine\n",
12480            "    persistence: ephemeral\n",
12481            "    shield:      Broker\n",
12482            "}",
12483        );
12484        let prog = parse(src);
12485        if let Declaration::Channel(ch) = &prog.declarations[0] {
12486            assert_eq!(ch.leading_trivia.len(), 1);
12487            assert!(ch.leading_trivia[0].is_doc());
12488            assert_eq!(ch.leading_trivia[0].text, "/// inbound order events");
12489        } else {
12490            panic!("expected Channel declaration");
12491        }
12492    }
12493
12494    #[test]
12495    fn per_struct_fields_match_side_channel() {
12496        // 14.a side-channel and 14.b per-struct fields must hold identical
12497        // data — they are populated by the same parser pass.
12498        let src = "/// for A\n// header for B\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }";
12499        let prog = parse(src);
12500        for (idx, decl) in prog.declarations.iter().enumerate() {
12501            let side = &prog.declaration_trivia[idx];
12502            let (per_lead, per_trail) = match decl {
12503                Declaration::Flow(f) => (&f.leading_trivia, &f.trailing_trivia),
12504                _ => panic!("unexpected variant"),
12505            };
12506            assert_eq!(per_lead.len(), side.leading.len());
12507            assert_eq!(per_trail.len(), side.trailing.len());
12508            for (a, b) in per_lead.iter().zip(side.leading.iter()) {
12509                assert_eq!(a.text, b.text);
12510                assert_eq!(a.kind, b.kind);
12511            }
12512        }
12513    }
12514
12515    #[test]
12516    fn comment_free_program_yields_empty_per_struct_fields() {
12517        let prog = parse("flow F() -> Out { }");
12518        if let Declaration::Flow(f) = &prog.declarations[0] {
12519            assert!(f.leading_trivia.is_empty());
12520            assert!(f.trailing_trivia.is_empty());
12521        } else {
12522            panic!("expected Flow declaration");
12523        }
12524    }
12525}
12526
12527// ── §Fase 14.c — inner doc comments (//!, /*!) ──────────────────────────────
12528//
12529// Inner doc comments document the *enclosing* item rather than the next
12530// sibling. Today they flow through the trivia channel like any other
12531// comment; downstream consumers (axon doc, LSP) decide how to interpret
12532// `is_inner_doc()`. These tests verify the lexer→parser pipeline preserves
12533// the inner-doc discriminator end-to-end.
12534
12535#[cfg(test)]
12536mod fase14c_inner_doc_tests {
12537    use super::*;
12538    use crate::lexer::Lexer;
12539    use crate::tokens::TriviaKind;
12540
12541    fn parse(src: &str) -> Program {
12542        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
12543        Parser::new(toks).parse().expect("parse")
12544    }
12545
12546    #[test]
12547    fn inner_doc_line_reaches_leading_trivia() {
12548        let src = "//! file-level docs\nflow F() -> Out { }";
12549        let prog = parse(src);
12550        let triv = &prog.declaration_trivia[0];
12551        assert_eq!(triv.leading.len(), 1);
12552        assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocLine);
12553        assert!(triv.leading[0].is_doc());
12554        assert!(triv.leading[0].is_inner_doc());
12555        assert_eq!(triv.leading[0].text, "//! file-level docs");
12556        assert_eq!(triv.leading[0].stripped_text(), " file-level docs");
12557    }
12558
12559    #[test]
12560    fn inner_doc_block_reaches_leading_trivia() {
12561        let src = "/*! module-level docs */\nflow F() -> Out { }";
12562        let prog = parse(src);
12563        let triv = &prog.declaration_trivia[0];
12564        assert_eq!(triv.leading.len(), 1);
12565        assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocBlock);
12566        assert!(triv.leading[0].is_inner_doc());
12567        assert_eq!(triv.leading[0].stripped_text(), " module-level docs ");
12568    }
12569
12570    #[test]
12571    fn outer_and_inner_doc_can_coexist() {
12572        // File-level inner doc on top, then an outer doc for the
12573        // declaration. Both reach the trivia channel and remain
12574        // distinguishable via `is_inner_doc()`.
12575        let src = "//! file docs\n/// docs F\nflow F() -> Out { }";
12576        let prog = parse(src);
12577        let triv = &prog.declaration_trivia[0];
12578        assert_eq!(triv.leading.len(), 2);
12579        assert!(triv.leading[0].is_inner_doc());
12580        assert!(triv.leading[1].is_doc());
12581        assert!(!triv.leading[1].is_inner_doc());
12582    }
12583
12584    #[test]
12585    fn inner_doc_reaches_per_struct_fields() {
12586        // Same data must be visible via the per-struct fields (Fase 14.b).
12587        let src = "//! intro\nflow F() -> Out { }";
12588        let prog = parse(src);
12589        if let Declaration::Flow(f) = &prog.declarations[0] {
12590            assert_eq!(f.leading_trivia.len(), 1);
12591            assert!(f.leading_trivia[0].is_inner_doc());
12592        } else {
12593            panic!("expected Flow declaration");
12594        }
12595    }
12596}
12597
12598// ── §Fase 28.c — Parser error recovery test pack ─────────────────────────────
12599//
12600// Mirror of `tests/test_fase28_parser_recovery.py` (Python side, 28.b).
12601// The test classes here line up 1-1 with the Python ones so the cross-
12602// stack drift gate (28.i) can compare error-list shapes input-for-input.
12603//
12604// Test classes:
12605//   - backwards_compat: existing `parse()` API unchanged
12606//   - single_error_recovery: one bad decl → one error, rest parse OK
12607//   - multi_error_recovery: N independent errors → N entries
12608//   - sync_points: every top-level keyword resyncs correctly
12609//   - parse_result_api: `has_errors`, `is_clean`
12610//   - edge_cases: EOF mid-error, brace imbalance, only-bad-tokens
12611//   - robustness_fuzz: 1000 deterministic-seeded mutations never crash
12612//   - no_ghost_errors: single broken field produces exactly 1 error
12613//   - integration_with_colon_diagnostic: v1.19.4 hint preserved under
12614//     recovery mode
12615#[cfg(test)]
12616mod fase28_recovery_tests {
12617    use super::*;
12618    use crate::lexer::Lexer;
12619
12620    /// Lex a source and return tokens for the parser to consume.
12621    /// Mirrors the Python `_parse_recovery` helper.
12622    fn lex(src: &str) -> Vec<Token> {
12623        Lexer::new(src, "<test>").tokenize().expect("lex")
12624    }
12625
12626    /// Parse with recovery mode. Returns `(program, errors)` so call
12627    /// sites read like the Python helper.
12628    fn recover(src: &str) -> ParseResult {
12629        Parser::new(lex(src)).parse_with_recovery()
12630    }
12631
12632    /// Strict parse. Mirrors the Python `_parse_strict` helper.
12633    fn strict(src: &str) -> Result<Program, ParseError> {
12634        Parser::new(lex(src)).parse()
12635    }
12636
12637    // ── backwards_compat ─────────────────────────────────────────
12638
12639    #[test]
12640    fn strict_parse_unchanged_for_clean_source() {
12641        // The existing `parse()` API must continue to succeed
12642        // verbatim on every well-formed input — D9.
12643        let src = "intent I {}";
12644        let prog = strict(src).expect("clean parse");
12645        assert_eq!(prog.declarations.len(), 1);
12646    }
12647
12648    #[test]
12649    fn strict_parse_still_raises_on_first_error() {
12650        // D9 + D8: opt-in to recovery via `parse_with_recovery`;
12651        // strict mode must still bubble the first error.
12652        // (Using a parse-time error rather than a lex error — `@@@`
12653        // would be rejected by the lexer, which is out of scope.)
12654        let src = "flow F() { } not_a_keyword flow G() { }";
12655        let _ = strict(src).expect_err("must error fast in strict mode");
12656    }
12657
12658    #[test]
12659    fn recovery_clean_source_yields_no_errors() {
12660        let src = "flow F() { } flow G() { }";
12661        let pr = recover(src);
12662        assert!(pr.is_clean(), "errors: {:?}", pr.errors);
12663        assert_eq!(pr.program.declarations.len(), 2);
12664    }
12665
12666    // ── single_error_recovery ────────────────────────────────────
12667
12668    #[test]
12669    fn single_unknown_top_level_token_recovers() {
12670        // One garbage token at top level; rest must parse.
12671        let src = "garbage_token flow F() { } flow G() { }";
12672        let pr = recover(src);
12673        assert_eq!(pr.errors.len(), 1, "errors: {:?}", pr.errors);
12674        assert_eq!(pr.program.declarations.len(), 2);
12675    }
12676
12677    #[test]
12678    fn error_in_first_decl_does_not_block_second() {
12679        // `flow F` body refers to non-keyword `nope`; the error
12680        // recovery must skip to the next top-level keyword.
12681        let src = "flow F() { not_a_step nope } flow G() { }";
12682        let pr = recover(src);
12683        assert!(pr.has_errors(), "expected at least one error");
12684        // The second flow must be reachable.
12685        let names: Vec<&str> = pr
12686            .program
12687            .declarations
12688            .iter()
12689            .filter_map(|d| match d {
12690                Declaration::Flow(f) => Some(f.name.as_str()),
12691                _ => None,
12692            })
12693            .collect();
12694        assert!(names.contains(&"G"), "G not found among {names:?}");
12695    }
12696
12697    #[test]
12698    fn malformed_declaration_then_clean_intent_recovers() {
12699        let src = "flow @ () { } intent I {}";
12700        let pr = recover(src);
12701        assert!(pr.has_errors());
12702        let kinds: Vec<&str> = pr
12703            .program
12704            .declarations
12705            .iter()
12706            .map(|d| match d {
12707                Declaration::Intent(_) => "intent",
12708                Declaration::Flow(_) => "flow",
12709                _ => "other",
12710            })
12711            .collect();
12712        assert!(kinds.contains(&"intent"), "kinds: {kinds:?}");
12713    }
12714
12715    #[test]
12716    fn recovery_does_not_double_count_a_single_error() {
12717        // Regression for the "ghost error" pathology that surfaced
12718        // during 28.b dev: a nested-decl error must not also fire
12719        // an "Unexpected token at top level" from the outer loop.
12720        // The Rust grammar has stricter intra-flow requirements
12721        // than Python; the invariant we assert here is that the
12722        // outer loop emits zero "Unexpected token at top level"
12723        // errors after an inner step-shape error.
12724        let src = "flow F() { not_a_step }";
12725        let pr = recover(src);
12726        let outer_ghosts = pr
12727            .errors
12728            .iter()
12729            .filter(|e| e.message.contains("at top level"))
12730            .count();
12731        assert_eq!(outer_ghosts, 0, "ghost errors: {:?}", pr.errors);
12732    }
12733
12734    // ── multi_error_recovery ─────────────────────────────────────
12735
12736    #[test]
12737    fn three_independent_errors_yield_three_entries() {
12738        let src =
12739            "garbage1 flow F() { } garbage2 flow G() { } garbage3 flow H() { }";
12740        let pr = recover(src);
12741        assert_eq!(pr.errors.len(), 3, "errors: {:?}", pr.errors);
12742        assert_eq!(pr.program.declarations.len(), 3);
12743    }
12744
12745    #[test]
12746    fn all_errors_no_valid_declarations() {
12747        let src = "foo bar baz qux";
12748        let pr = recover(src);
12749        assert!(pr.has_errors());
12750        assert!(pr.program.declarations.is_empty());
12751    }
12752
12753    #[test]
12754    fn errors_recorded_in_source_order() {
12755        let src = "x flow A() { } y flow B() { } z flow C() { }";
12756        let pr = recover(src);
12757        assert_eq!(pr.errors.len(), 3);
12758        let lines: Vec<u32> = pr.errors.iter().map(|e| e.line).collect();
12759        // Same source-line means we compare by column ordering;
12760        // either way they must be non-decreasing.
12761        assert!(
12762            lines.windows(2).all(|w| w[0] <= w[1]),
12763            "errors out of order: {lines:?}"
12764        );
12765    }
12766
12767    // ── sync_points ──────────────────────────────────────────────
12768
12769    #[test]
12770    fn sync_to_flow_keyword() {
12771        let src = "garbage flow F() { }";
12772        let pr = recover(src);
12773        assert_eq!(pr.program.declarations.len(), 1);
12774    }
12775
12776    #[test]
12777    fn sync_to_intent_keyword() {
12778        let src = "garbage intent I {}";
12779        let pr = recover(src);
12780        assert_eq!(pr.program.declarations.len(), 1);
12781    }
12782
12783    #[test]
12784    fn sync_to_persona_keyword() {
12785        let src = "garbage persona P { name: \"P\" role: \"R\" }";
12786        let pr = recover(src);
12787        assert!(
12788            pr.program
12789                .declarations
12790                .iter()
12791                .any(|d| matches!(d, Declaration::Persona(_))),
12792            "persona not recovered: decls = {:?}",
12793            pr.program.declarations.len()
12794        );
12795    }
12796
12797    #[test]
12798    fn sync_to_run_keyword() {
12799        let src = "garbage run R { input: { user_message: \"hi\" } }";
12800        let pr = recover(src);
12801        // Either Run was parsed, or recovery still produced ≥1 err.
12802        assert!(pr.has_errors());
12803    }
12804
12805    // ── parse_result_api ─────────────────────────────────────────
12806
12807    #[test]
12808    fn parse_result_has_errors_and_is_clean_invert() {
12809        let pr_clean = recover("flow F() { }");
12810        assert!(pr_clean.is_clean());
12811        assert!(!pr_clean.has_errors());
12812
12813        let pr_err = recover("garbage");
12814        assert!(!pr_err.is_clean());
12815        assert!(pr_err.has_errors());
12816    }
12817
12818    #[test]
12819    fn parse_result_program_field_holds_partial_program() {
12820        let pr = recover("garbage flow F() { }");
12821        assert!(!pr.program.declarations.is_empty());
12822    }
12823
12824    #[test]
12825    fn parse_result_errors_carry_line_and_column() {
12826        let pr = recover("garbage");
12827        assert!(!pr.errors.is_empty());
12828        let e = &pr.errors[0];
12829        assert!(e.line >= 1);
12830        // Column may be 0-based or 1-based depending on lexer;
12831        // accept anything ≥ 0.
12832        let _ = e.column;
12833        assert!(!e.message.is_empty());
12834    }
12835
12836    #[test]
12837    fn parse_result_debug_renders() {
12838        let pr = recover("flow F() { }");
12839        let s = format!("{pr:?}");
12840        assert!(s.contains("ParseResult"));
12841    }
12842
12843    // ── edge_cases ───────────────────────────────────────────────
12844
12845    #[test]
12846    fn empty_source_is_clean() {
12847        let pr = recover("");
12848        assert!(pr.is_clean());
12849        assert!(pr.program.declarations.is_empty());
12850    }
12851
12852    #[test]
12853    fn whitespace_only_source_is_clean() {
12854        let pr = recover("   \n\n\t  \n");
12855        assert!(pr.is_clean());
12856        assert!(pr.program.declarations.is_empty());
12857    }
12858
12859    #[test]
12860    fn only_garbage_does_not_crash() {
12861        // Lex-clean garbage tokens (avoids AxonLexerError).
12862        let pr = recover("foo bar baz { qux quux } corge { grault }");
12863        assert!(pr.has_errors());
12864    }
12865
12866    #[test]
12867    fn unbalanced_close_brace_does_not_crash() {
12868        let pr = recover("} flow F() { }");
12869        // Recovery must keep walking past stray `}`.
12870        let names: Vec<&str> = pr
12871            .program
12872            .declarations
12873            .iter()
12874            .filter_map(|d| match d {
12875                Declaration::Flow(f) => Some(f.name.as_str()),
12876                _ => None,
12877            })
12878            .collect();
12879        assert!(names.contains(&"F"), "F not recovered: {names:?}");
12880    }
12881
12882    #[test]
12883    fn error_at_eof_does_not_loop() {
12884        // Truncated declaration. Must terminate; finite errors.
12885        let pr = recover("flow F() { ");
12886        // Either errored or somehow accepted — but must terminate.
12887        let _ = pr.errors.len();
12888    }
12889
12890    #[test]
12891    fn nested_braces_inside_error_still_balance() {
12892        // Walker must respect brace depth so a `}` inside a malformed
12893        // block does not prematurely sync.
12894        let src = "flow F() { not_a_step { inner } } flow G() { }";
12895        let pr = recover(src);
12896        let names: Vec<&str> = pr
12897            .program
12898            .declarations
12899            .iter()
12900            .filter_map(|d| match d {
12901                Declaration::Flow(f) => Some(f.name.as_str()),
12902                _ => None,
12903            })
12904            .collect();
12905        assert!(names.contains(&"G"), "G not recovered: {names:?}");
12906    }
12907
12908    // ── robustness_fuzz ──────────────────────────────────────────
12909    //
12910    // Deterministic-seeded mutator (xorshift). 100 buckets ×
12911    // 10 mutations = 1000 iterations, byte-bounded so fuzz time
12912    // stays under 1 s on a release build. Recovery must NEVER crash;
12913    // lexer-level errors are out of scope (lexer recovery is its own
12914    // sub-fase). 28.b mirrors this with the same structure.
12915
12916    #[derive(Clone, Copy)]
12917    struct Xorshift(u64);
12918    impl Xorshift {
12919        fn next(&mut self) -> u64 {
12920            let mut x = self.0;
12921            x ^= x << 13;
12922            x ^= x >> 7;
12923            x ^= x << 17;
12924            self.0 = x;
12925            x
12926        }
12927        fn pick<T: Copy>(&mut self, slice: &[T]) -> T {
12928            slice[(self.next() as usize) % slice.len()]
12929        }
12930    }
12931
12932    fn mutate(src: &str, rng: &mut Xorshift) -> String {
12933        let mut bytes: Vec<u8> = src.bytes().collect();
12934        if bytes.is_empty() {
12935            return src.to_string();
12936        }
12937        let op = rng.next() % 4;
12938        let pos = (rng.next() as usize) % bytes.len();
12939        // Stick to ASCII-safe printable bytes to keep input lex-able
12940        // most of the time. AxonLexerError is still possible and is
12941        // tolerated by the recovery contract.
12942        let safe: &[u8] = b"abcdefghijklmnopqrstuvwxyz {}();:,_0123456789";
12943        match op {
12944            0 => {
12945                bytes.remove(pos);
12946            }
12947            1 => {
12948                let b = rng.pick(safe);
12949                bytes.insert(pos, b);
12950            }
12951            2 if pos + 1 < bytes.len() => {
12952                bytes.swap(pos, pos + 1);
12953            }
12954            _ => {
12955                let b = rng.pick(safe);
12956                bytes[pos] = b;
12957            }
12958        }
12959        // Lossy decode: mutator may have produced invalid UTF-8;
12960        // strip non-ASCII before handing to the lexer.
12961        bytes.retain(|b| b.is_ascii());
12962        String::from_utf8_lossy(&bytes).into_owned()
12963    }
12964
12965    #[test]
12966    fn fuzz_recovery_never_crashes() {
12967        let seed_bases = [
12968            "flow F() { }",
12969            "intent I { }",
12970            "persona P { name: \"P\" role: \"R\" }",
12971            "intent J { ask: \"a\" }",
12972            "type T = String",
12973        ];
12974        // 100 buckets × 10 mutations = 1000 iterations, deterministic.
12975        for (bucket, base) in (0..100u64).zip(seed_bases.iter().cycle()) {
12976            let mut rng = Xorshift(0x1234_5678_9abc_def0_u64.wrapping_add(bucket));
12977            let mut current = (*base).to_string();
12978            for _ in 0..10 {
12979                current = mutate(&current, &mut rng);
12980                // Lexer may reject; that's outside parser-recovery
12981                // scope (28.b/c). Skip those iterations.
12982                let toks = match Lexer::new(&current, "<fuzz>").tokenize() {
12983                    Ok(t) => t,
12984                    Err(_) => continue,
12985                };
12986                // Recovery must not panic on any well-lexed input.
12987                let _pr = Parser::new(toks).parse_with_recovery();
12988            }
12989        }
12990    }
12991
12992    // ── integration_with_v1_19_4_colon_diagnostic ────────────────
12993
12994    #[test]
12995    fn missing_colon_hint_preserved_under_recovery() {
12996        // The Rust frontend's strict `parse()` carries the same
12997        // colon diagnostic shape as the Python side. Recovery mode
12998        // must not erase it.
12999        let src = "flow F() { run R { input { user_message: \"hi\" } } }";
13000        let pr = recover(src);
13001        // Either the parser accepts this (some shape may be valid)
13002        // or it errors — but if it errors, the message must surface
13003        // the diagnostic content.
13004        if !pr.errors.is_empty() {
13005            let any_msg = pr.errors.iter().any(|e| !e.message.is_empty());
13006            assert!(any_msg);
13007        }
13008    }
13009
13010    // ── recovery preserves declaration ordering ──────────────────
13011
13012    #[test]
13013    fn recovered_declarations_appear_in_source_order() {
13014        let src = "flow A() { } garbage flow B() { } garbage flow C() { }";
13015        let pr = recover(src);
13016        let names: Vec<&str> = pr
13017            .program
13018            .declarations
13019            .iter()
13020            .filter_map(|d| match d {
13021                Declaration::Flow(f) => Some(f.name.as_str()),
13022                _ => None,
13023            })
13024            .collect();
13025        assert_eq!(names, vec!["A", "B", "C"]);
13026    }
13027}
13028
13029// ── §Fase 28.d — Source-context diagnostic block test pack ───────────────────
13030//
13031// Mirror of `tests/test_fase28_source_context.py` (Python side, 28.d).
13032// The render output must be byte-identical to the Python `SourceSnippet.render`
13033// on the same input — D7 ratified (cross-stack drift gate). Golden strings
13034// in `golden_*` tests are duplicated verbatim in the Python pack; edits
13035// here MUST be mirrored on the Python side and vice versa.
13036#[cfg(test)]
13037mod fase28_source_context_tests {
13038    use super::*;
13039    use crate::lexer::Lexer;
13040
13041    fn snippet(source: &str, line: u32, column: u32, filename: &str) -> String {
13042        SourceSnippet::new(
13043            source.to_string(),
13044            line,
13045            column,
13046            filename.to_string(),
13047        )
13048        .render()
13049    }
13050
13051    // ── Pure rendering ──────────────────────────────────────────
13052
13053    #[test]
13054    fn rustc_style_block_for_middle_line() {
13055        let src = "line one\nline two\nline three\nline four\nline five";
13056        let out = snippet(src, 3, 6, "x.axon");
13057        assert!(out.contains("--> x.axon:3:6"));
13058        assert!(out.contains("1 | line one"));
13059        assert!(out.contains("2 | line two"));
13060        assert!(out.contains("3 | line three"));
13061        assert!(out.contains("4 | line four"));
13062        assert!(out.contains("5 | line five"));
13063        // Caret col 6 → 5-space pad. Empty gutter is 1 space (gutter=1).
13064        assert!(out.contains("\n  |      ^"), "out:\n{out}");
13065    }
13066
13067    #[test]
13068    fn caret_column_one_renders_correctly() {
13069        let out = snippet("abc\n", 1, 1, "<source>");
13070        assert!(out.contains("\n  | ^"));
13071    }
13072
13073    #[test]
13074    fn first_line_clamps_context_before_to_zero() {
13075        let src = "first\nsecond\nthird\nfourth\nfifth";
13076        let out = snippet(src, 1, 1, "<source>");
13077        assert!(out.contains("1 | first"));
13078        assert!(out.contains("2 | second"));
13079        assert!(out.contains("3 | third"));
13080        assert!(!out.contains("4 | fourth"));
13081    }
13082
13083    #[test]
13084    fn last_line_clamps_context_after_to_eof() {
13085        let src = "first\nsecond\nthird\nfourth\nfifth";
13086        let out = snippet(src, 5, 2, "<source>");
13087        assert!(out.contains("5 | fifth"));
13088        assert!(out.contains("3 | third"));
13089        assert!(out.contains("4 | fourth"));
13090        assert!(!out.contains("2 | second"));
13091    }
13092
13093    #[test]
13094    fn gutter_width_grows_with_line_count() {
13095        let src: String = (1..=12).map(|i| format!("line{i}")).collect::<Vec<_>>().join("\n");
13096        let out = snippet(&src, 12, 1, "<source>");
13097        assert!(out.contains("12 | line12"));
13098        assert!(out.contains("10 | line10"));
13099    }
13100
13101    // ── Edge cases ──────────────────────────────────────────────
13102
13103    #[test]
13104    fn empty_source_returns_empty() {
13105        assert_eq!(snippet("", 1, 1, "<source>"), "");
13106    }
13107
13108    #[test]
13109    fn zero_line_returns_empty() {
13110        assert_eq!(snippet("hi", 0, 1, "<source>"), "");
13111    }
13112
13113    #[test]
13114    fn out_of_range_line_returns_empty() {
13115        assert_eq!(snippet("hi", 99, 1, "<source>"), "");
13116    }
13117
13118    #[test]
13119    fn caret_clamps_past_eol() {
13120        let out = snippet("hello", 1, 50, "<source>");
13121        assert!(out.contains("\n  |      ^"), "out:\n{out}");
13122    }
13123
13124    #[test]
13125    fn unicode_codepoint_count_for_caret_clamp() {
13126        // "héllo" = 5 codepoints; column past EOL clamps to 6.
13127        let out = snippet("héllo", 1, 99, "<source>");
13128        assert!(out.contains("\n  |      ^"), "out:\n{out}");
13129    }
13130
13131    #[test]
13132    fn trailing_newline_does_not_create_phantom_last_line() {
13133        let out = snippet("first\nsecond\n", 2, 1, "<source>");
13134        assert!(!out.contains("3 |"));
13135        assert!(out.contains("2 | second"));
13136    }
13137
13138    // ── Parser attach plumbing ──────────────────────────────────
13139
13140    fn lex(src: &str) -> Vec<Token> {
13141        Lexer::new(src, "<test>").tokenize().expect("lex")
13142    }
13143
13144    #[test]
13145    fn strict_parse_attaches_snippet_when_source_given() {
13146        let src = "garbage_token\nflow F() { }";
13147        let err = Parser::new(lex(src))
13148            .with_source(src, "x.axon")
13149            .parse()
13150            .expect_err("must error");
13151        assert!(err.source_snippet.is_some());
13152        let display = format!("{err}");
13153        assert!(display.contains("--> x.axon:"), "display: {display}");
13154    }
13155
13156    #[test]
13157    fn strict_parse_no_snippet_when_no_source() {
13158        let src = "garbage_token";
13159        let err = Parser::new(lex(src)).parse().expect_err("must error");
13160        assert!(err.source_snippet.is_none());
13161        let display = format!("{err}");
13162        assert!(!display.contains("\n  -->"));
13163    }
13164
13165    #[test]
13166    fn every_recovered_error_has_snippet() {
13167        let src = "garbage1\nflow F() { }\ngarbage2\nflow G() { }";
13168        let result = Parser::new(lex(src))
13169            .with_source(src, "multi.axon")
13170            .parse_with_recovery();
13171        assert!(!result.errors.is_empty());
13172        for err in &result.errors {
13173            assert!(err.source_snippet.is_some());
13174            let display = format!("{err}");
13175            assert!(
13176                display.contains("--> multi.axon:"),
13177                "display: {display}"
13178            );
13179        }
13180    }
13181
13182    #[test]
13183    fn recovery_no_snippet_when_no_source() {
13184        let src = "garbage1 garbage2";
13185        let result = Parser::new(lex(src)).parse_with_recovery();
13186        for err in &result.errors {
13187            assert!(err.source_snippet.is_none());
13188        }
13189    }
13190
13191    #[test]
13192    fn snippet_points_at_correct_line_for_each_error() {
13193        let src = "garbage_a\nflow F() { }\ngarbage_b\nflow G() { }";
13194        let result = Parser::new(lex(src))
13195            .with_source(src, "x")
13196            .parse_with_recovery();
13197        for err in &result.errors {
13198            let sn = err.source_snippet.as_ref().expect("snippet");
13199            assert_eq!(sn.line, err.line);
13200        }
13201    }
13202
13203    // ── Backwards-compat ────────────────────────────────────────
13204
13205    #[test]
13206    fn legacy_constructor_still_works() {
13207        let src = "flow F() { }";
13208        let prog = Parser::new(lex(src)).parse().expect("clean");
13209        assert_eq!(prog.declarations.len(), 1);
13210    }
13211
13212    #[test]
13213    fn attach_source_idempotent() {
13214        let err = ParseError {
13215            message: "bad".to_string(),
13216            line: 2,
13217            column: 3,
13218            ..Default::default()
13219        };
13220        let err2 = err.clone().attach_source("a\nb\nc\n", "f.axon");
13221        let first = format!("{err2}");
13222        let err3 = err.attach_source("a\nb\nc\n", "f.axon");
13223        let second = format!("{err3}");
13224        assert_eq!(first, second);
13225    }
13226
13227    #[test]
13228    fn attach_source_noop_when_line_zero() {
13229        let err = ParseError {
13230            message: "bad".to_string(),
13231            line: 0,
13232            column: 0,
13233            ..Default::default()
13234        };
13235        let err = err.attach_source("a\nb\nc\n", "f.axon");
13236        assert!(err.source_snippet.is_none());
13237    }
13238
13239    // ── Cross-stack golden parity ───────────────────────────────
13240    // These golden strings are duplicated verbatim in the Python
13241    // test pack at `tests/test_fase28_source_context.py::TestRustParityShape`.
13242    // Edits here MUST be mirrored in the Python pack — D7.
13243
13244    #[test]
13245    fn golden_simple_three_line_block() {
13246        let src = "alpha\nbeta\ngamma";
13247        let out = snippet(src, 2, 3, "g.axon");
13248        // Note: gutter=1, so empty_gutter=" " (one space). The
13249        // " --> ..." line therefore starts with two spaces ("<empty>"
13250        // + literal " --> ...").
13251        let expected = concat!(
13252            "  --> g.axon:2:3\n",
13253            "  |\n",
13254            "1 | alpha\n",
13255            "2 | beta\n",
13256            "  |   ^\n",
13257            "3 | gamma",
13258        );
13259        assert_eq!(out, expected);
13260    }
13261
13262    #[test]
13263    fn golden_first_line_caret() {
13264        let src = "abc\ndef\n";
13265        let out = snippet(src, 1, 1, "x");
13266        let expected = concat!(
13267            "  --> x:1:1\n",
13268            "  |\n",
13269            "1 | abc\n",
13270            "  | ^\n",
13271            "2 | def",
13272        );
13273        assert_eq!(out, expected);
13274    }
13275
13276    #[test]
13277    fn golden_two_digit_gutter() {
13278        let src: String = (1..=11)
13279            .map(|i| format!("L{i}"))
13280            .collect::<Vec<_>>()
13281            .join("\n");
13282        let out = snippet(&src, 10, 2, "big");
13283        let expected = concat!(
13284            "   --> big:10:2\n",
13285            "   |\n",
13286            " 8 | L8\n",
13287            " 9 | L9\n",
13288            "10 | L10\n",
13289            "   |  ^\n",
13290            "11 | L11",
13291        );
13292        assert_eq!(out, expected);
13293    }
13294}
13295
13296// ── §Fase 28.e — Parser integration tests for smart-suggest ──────────────────
13297//
13298// Mirror of `tests/test_fase28_smart_suggest.py::TestParserIntegration`.
13299// Verifies that the parser actually wires `suggest_for` into the
13300// unknown-keyword diagnostic at both error sites — top-level and
13301// flow-body.
13302#[cfg(test)]
13303mod fase28_smart_suggest_parser_tests {
13304    use super::*;
13305    use crate::lexer::Lexer;
13306
13307    fn lex(src: &str) -> Vec<Token> {
13308        Lexer::new(src, "<test>").tokenize().expect("lex")
13309    }
13310
13311    #[test]
13312    fn top_level_typo_suggests_flow() {
13313        let src = "flwo F() { }";
13314        let err = Parser::new(lex(src)).parse().expect_err("must error");
13315        assert!(
13316            err.message.contains("Did you mean `flow`?"),
13317            "msg: {}",
13318            err.message
13319        );
13320    }
13321
13322    #[test]
13323    fn top_level_unknown_far_no_suggestion() {
13324        let src = "qwerty F() { }";
13325        let err = Parser::new(lex(src)).parse().expect_err("must error");
13326        assert!(
13327            !err.message.contains("Did you mean"),
13328            "msg: {}",
13329            err.message
13330        );
13331    }
13332
13333    #[test]
13334    fn flow_body_typo_suggests_step() {
13335        let src = "flow F() { stepp S {} }";
13336        let err = Parser::new(lex(src)).parse().expect_err("must error");
13337        assert!(
13338            err.message.contains("Did you mean `step`"),
13339            "msg: {}",
13340            err.message
13341        );
13342    }
13343
13344    #[test]
13345    fn flow_body_typo_suggests_reason() {
13346        let src = "flow F() { reasn R {} }";
13347        let err = Parser::new(lex(src)).parse().expect_err("must error");
13348        assert!(
13349            err.message.contains("Did you mean `reason`?"),
13350            "msg: {}",
13351            err.message
13352        );
13353    }
13354
13355    #[test]
13356    fn recovery_mode_carries_hint() {
13357        let src = "flwo F() { }";
13358        let result = Parser::new(lex(src)).parse_with_recovery();
13359        assert!(
13360            result
13361                .errors
13362                .iter()
13363                .any(|e| e.message.contains("Did you mean `flow`?")),
13364            "errors: {:?}",
13365            result.errors
13366        );
13367    }
13368}
13369
13370// ── §Fase 35.m — mutate / purge where-clause capture ────────────────
13371
13372#[cfg(test)]
13373mod fase35m_mutate_purge_where_tests {
13374    use super::*;
13375
13376    fn parse(src: &str) -> Program {
13377        let tokens = crate::lexer::Lexer::new(src, "<test>")
13378            .tokenize()
13379            .expect("lex");
13380        Parser::new(tokens).parse().expect("parse")
13381    }
13382
13383    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
13384        for d in &prog.declarations {
13385            if let Declaration::Flow(f) = d {
13386                if f.name == flow {
13387                    return f.body.first().expect("flow has at least one step");
13388                }
13389            }
13390        }
13391        panic!("flow `{flow}` not found");
13392    }
13393
13394    #[test]
13395    fn mutate_captures_its_where_clause() {
13396        // Pre-35.m the `{ where: }` block was skipped — every mutate
13397        // ran whole-store. It must now reach `where_expr`.
13398        let prog =
13399            parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
13400        match first_step(&prog, "F") {
13401            FlowStep::Mutate(m) => {
13402                assert_eq!(m.store_name, "accounts");
13403                assert_eq!(m.where_expr, "id = 1");
13404            }
13405            other => panic!("expected Mutate, got {other:?}"),
13406        }
13407    }
13408
13409    #[test]
13410    fn purge_captures_its_where_clause() {
13411        let prog =
13412            parse("flow F() -> Unit { purge logs { where: \"ts < 100\" } }");
13413        match first_step(&prog, "F") {
13414            FlowStep::Purge(p) => {
13415                assert_eq!(p.store_name, "logs");
13416                assert_eq!(p.where_expr, "ts < 100");
13417            }
13418            other => panic!("expected Purge, got {other:?}"),
13419        }
13420    }
13421
13422    #[test]
13423    fn mutate_without_a_where_block_is_a_whole_store_op() {
13424        // No `{ where: }` → an empty filter → the runtime renders
13425        // `WHERE TRUE` (every row). A valid, intentional op.
13426        let prog = parse("flow F() -> Unit { mutate accounts }");
13427        match first_step(&prog, "F") {
13428            FlowStep::Mutate(m) => {
13429                assert_eq!(m.store_name, "accounts");
13430                assert_eq!(m.where_expr, "");
13431            }
13432            other => panic!("expected Mutate, got {other:?}"),
13433        }
13434    }
13435}
13436
13437// ── §Fase 35.o — persist field-block capture ────────────────────────
13438
13439#[cfg(test)]
13440mod fase35o_persist_fields_tests {
13441    use super::*;
13442
13443    fn parse(src: &str) -> Program {
13444        let tokens = crate::lexer::Lexer::new(src, "<test>")
13445            .tokenize()
13446            .expect("lex");
13447        Parser::new(tokens).parse().expect("parse")
13448    }
13449
13450    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
13451        for d in &prog.declarations {
13452            if let Declaration::Flow(f) = d {
13453                if f.name == flow {
13454                    return f.body.first().expect("flow has at least one step");
13455                }
13456            }
13457        }
13458        panic!("flow `{flow}` not found");
13459    }
13460
13461    #[test]
13462    fn persist_captures_its_field_block() {
13463        // Pre-35.o the `{ col: value }` block was skipped — every
13464        // persist wrote the whole binding context. It must now reach
13465        // `fields`, in source order, with value expressions raw.
13466        let prog = parse(
13467            "flow F() -> Unit { persist into chat_history { \
13468             session_id: \"${session_id}\" sender: \"user\" \
13469             content: \"${message}\" } }",
13470        );
13471        match first_step(&prog, "F") {
13472            FlowStep::Persist(p) => {
13473                assert_eq!(p.store_name, "chat_history");
13474                assert_eq!(
13475                    p.fields,
13476                    vec![
13477                        ("session_id".to_string(), "${session_id}".to_string()),
13478                        ("sender".to_string(), "user".to_string()),
13479                        ("content".to_string(), "${message}".to_string()),
13480                    ]
13481                );
13482            }
13483            other => panic!("expected Persist, got {other:?}"),
13484        }
13485    }
13486
13487    #[test]
13488    fn persist_without_a_block_keeps_the_user_bindings_fallback() {
13489        // No `{ }` → empty `fields` → the runtime falls back to the
13490        // v1.30.0 user-bindings row. Backward-compatible.
13491        let prog = parse("flow F() -> Unit { persist events }");
13492        match first_step(&prog, "F") {
13493            FlowStep::Persist(p) => {
13494                assert_eq!(p.store_name, "events");
13495                assert!(p.fields.is_empty());
13496            }
13497            other => panic!("expected Persist, got {other:?}"),
13498        }
13499    }
13500
13501    #[test]
13502    fn persist_accepts_the_optional_into_connector() {
13503        // `persist into X` and `persist X` resolve to the SAME store
13504        // name — pre-35.o `into` was captured AS the store name.
13505        let with =
13506            parse("flow F() -> Unit { persist into accounts { id: \"1\" } }");
13507        let without =
13508            parse("flow F() -> Unit { persist accounts { id: \"1\" } }");
13509        for prog in [&with, &without] {
13510            match first_step(prog, "F") {
13511                FlowStep::Persist(p) => assert_eq!(p.store_name, "accounts"),
13512                other => panic!("expected Persist, got {other:?}"),
13513            }
13514        }
13515    }
13516
13517    #[test]
13518    fn persist_into_without_a_block_resolves_the_store_name() {
13519        // `persist into events` — the `into` connector is skipped, the
13520        // store name is `events` (not `into`). Lateral bug closed.
13521        let prog = parse("flow F() -> Unit { persist into events }");
13522        match first_step(&prog, "F") {
13523            FlowStep::Persist(p) => {
13524                assert_eq!(p.store_name, "events");
13525                assert!(p.fields.is_empty());
13526            }
13527            other => panic!("expected Persist, got {other:?}"),
13528        }
13529    }
13530
13531    #[test]
13532    fn persist_fields_lower_into_the_ir() {
13533        // The IR generator must carry `fields` onto `IRPersistStep`
13534        // so the runtime reads exactly the declared columns.
13535        let prog = parse(
13536            "flow F() -> Unit { persist into chat { content: \"${msg}\" } }",
13537        );
13538        let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
13539        let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
13540        match flow.steps.first().expect("one step") {
13541            crate::ir_nodes::IRFlowNode::Persist(p) => {
13542                assert_eq!(p.store_name, "chat");
13543                assert_eq!(
13544                    p.fields,
13545                    vec![("content".to_string(), "${msg}".to_string())]
13546                );
13547            }
13548            other => panic!("expected IRFlowNode::Persist, got {other:?}"),
13549        }
13550    }
13551}
13552
13553// ── §Fase 35.p — mutate SET-field-block capture ─────────────────────
13554
13555#[cfg(test)]
13556mod fase35p_mutate_fields_tests {
13557    use super::*;
13558
13559    fn parse(src: &str) -> Program {
13560        let tokens = crate::lexer::Lexer::new(src, "<test>")
13561            .tokenize()
13562            .expect("lex");
13563        Parser::new(tokens).parse().expect("parse")
13564    }
13565
13566    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
13567        for d in &prog.declarations {
13568            if let Declaration::Flow(f) = d {
13569                if f.name == flow {
13570                    return f.body.first().expect("flow has at least one step");
13571                }
13572            }
13573        }
13574        panic!("flow `{flow}` not found");
13575    }
13576
13577    #[test]
13578    fn mutate_captures_its_set_field_block() {
13579        // Pre-35.p every key but `where:` was skipped — the runtime
13580        // SET every flow binding. The SET columns must now reach
13581        // `fields`, in source order, with `where:` still captured.
13582        let prog = parse(
13583            "flow F() -> Unit { mutate accounts { where: \"id = ${id}\" \
13584             balance: \"${new_balance}\" status: \"active\" } }",
13585        );
13586        match first_step(&prog, "F") {
13587            FlowStep::Mutate(m) => {
13588                assert_eq!(m.store_name, "accounts");
13589                assert_eq!(m.where_expr, "id = ${id}");
13590                assert_eq!(
13591                    m.fields,
13592                    vec![
13593                        ("balance".to_string(), "${new_balance}".to_string()),
13594                        ("status".to_string(), "active".to_string()),
13595                    ]
13596                );
13597            }
13598            other => panic!("expected Mutate, got {other:?}"),
13599        }
13600    }
13601
13602    #[test]
13603    fn mutate_where_only_block_has_no_set_fields() {
13604        // A `{ where: }`-only block → empty `fields` → the runtime
13605        // falls back to the v1.31.0 user-bindings SET.
13606        let prog =
13607            parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
13608        match first_step(&prog, "F") {
13609            FlowStep::Mutate(m) => {
13610                assert_eq!(m.where_expr, "id = 1");
13611                assert!(m.fields.is_empty());
13612            }
13613            other => panic!("expected Mutate, got {other:?}"),
13614        }
13615    }
13616
13617    #[test]
13618    fn mutate_with_no_block_is_a_whole_store_op() {
13619        // No block at all → empty where + empty fields (a whole-store
13620        // UPDATE from user bindings) — unchanged from 35.m.
13621        let prog = parse("flow F() -> Unit { mutate accounts }");
13622        match first_step(&prog, "F") {
13623            FlowStep::Mutate(m) => {
13624                assert_eq!(m.store_name, "accounts");
13625                assert_eq!(m.where_expr, "");
13626                assert!(m.fields.is_empty());
13627            }
13628            other => panic!("expected Mutate, got {other:?}"),
13629        }
13630    }
13631
13632    #[test]
13633    fn mutate_fields_lower_into_the_ir() {
13634        let prog = parse(
13635            "flow F() -> Unit { mutate t { where: \"id = 1\" v: \"${x}\" } }",
13636        );
13637        let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
13638        let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
13639        match flow.steps.first().expect("one step") {
13640            crate::ir_nodes::IRFlowNode::Mutate(m) => {
13641                assert_eq!(m.where_expr, "id = 1");
13642                assert_eq!(
13643                    m.fields,
13644                    vec![("v".to_string(), "${x}".to_string())]
13645                );
13646            }
13647            other => panic!("expected IRFlowNode::Mutate, got {other:?}"),
13648        }
13649    }
13650}
13651