Skip to main content

axon_frontend/
parser.rs

1//! AXON Parser — recursive descent, fail-fast.
2//!
3//! Direct port of axon/compiler/parser.py.
4//!
5//! Tier 1 constructs (persona, context, anchor, memory, tool, type,
6//! flow, step, intent, run, epistemic, if, for, let, return) are
7//! fully parsed into typed AST nodes.
8//!
9//! Tier 2+ constructs are parsed structurally (balanced braces) into
10//! `GenericDeclaration` / `GenericFlowStep`.
11
12use crate::ast::*;
13use crate::tokens::{is_declaration_keyword, Token, TokenType, Trivia, TriviaKind};
14
15// Comment token kinds the lexer now emits (Fase 14.a). The parser
16// filters these out of its working stream — they are materialised into
17// a parallel `Trivia` array indexed by effective-token position, then
18// attached to `Program.declaration_trivia[i]` once each declaration's
19// span is known.
20const fn is_comment_token(tt: &TokenType) -> bool {
21    matches!(
22        tt,
23        TokenType::LineComment
24            | TokenType::BlockComment
25            | TokenType::DocLineComment
26            | TokenType::DocBlockComment
27            | TokenType::InnerDocLineComment
28            | TokenType::InnerDocBlockComment
29    )
30}
31
32const fn token_to_trivia_kind(tt: &TokenType) -> Option<TriviaKind> {
33    match tt {
34        TokenType::LineComment => Some(TriviaKind::Line),
35        TokenType::BlockComment => Some(TriviaKind::Block),
36        TokenType::DocLineComment => Some(TriviaKind::DocLine),
37        TokenType::DocBlockComment => Some(TriviaKind::DocBlock),
38        TokenType::InnerDocLineComment => Some(TriviaKind::InnerDocLine),
39        TokenType::InnerDocBlockComment => Some(TriviaKind::InnerDocBlock),
40        _ => None,
41    }
42}
43
44/// Fase 14.b — write `leading_trivia` and `trailing_trivia` into the
45/// per-struct fields of a `Declaration` variant.
46///
47/// Mirrors what the Python parser does automatically via its
48/// `_with_trivia` decorator on every `_parse_*` method. In Rust we
49/// do it once at the top of the parse loop so the spread to every
50/// variant is in a single place.
51fn attach_trivia_to_decl(decl: &mut Declaration, leading: Vec<Trivia>, trailing: Vec<Trivia>) {
52    match decl {
53        // §Fase 114.a — a top-level `budget` carries its comments like any other
54        // declaration.
55        Declaration::Budget(n) => {
56            n.leading_trivia = leading;
57            n.trailing_trivia = trailing;
58        }
59        Declaration::Import(n) => {
60            n.leading_trivia = leading;
61            n.trailing_trivia = trailing;
62        }
63        Declaration::Persona(n) => {
64            n.leading_trivia = leading;
65            n.trailing_trivia = trailing;
66        }
67        Declaration::Context(n) => {
68            n.leading_trivia = leading;
69            n.trailing_trivia = trailing;
70        }
71        Declaration::Anchor(n) => {
72            n.leading_trivia = leading;
73            n.trailing_trivia = trailing;
74        }
75        Declaration::Memory(n) => {
76            n.leading_trivia = leading;
77            n.trailing_trivia = trailing;
78        }
79        Declaration::Tool(n) => {
80            n.leading_trivia = leading;
81            n.trailing_trivia = trailing;
82        }
83        Declaration::Type(n) => {
84            n.leading_trivia = leading;
85            n.trailing_trivia = trailing;
86        }
87        Declaration::Flow(n) => {
88            n.leading_trivia = leading;
89            n.trailing_trivia = trailing;
90        }
91        Declaration::Intent(n) => {
92            n.leading_trivia = leading;
93            n.trailing_trivia = trailing;
94        }
95        Declaration::Run(n) => {
96            n.leading_trivia = leading;
97            n.trailing_trivia = trailing;
98        }
99        Declaration::Epistemic(n) => {
100            n.leading_trivia = leading;
101            n.trailing_trivia = trailing;
102        }
103        Declaration::Let(n) => {
104            n.leading_trivia = leading;
105            n.trailing_trivia = trailing;
106        }
107        Declaration::LambdaData(n) => {
108            n.leading_trivia = leading;
109            n.trailing_trivia = trailing;
110        }
111        Declaration::Agent(n) => {
112            n.leading_trivia = leading;
113            n.trailing_trivia = trailing;
114        }
115        Declaration::Shield(n) => {
116            n.leading_trivia = leading;
117            n.trailing_trivia = trailing;
118        }
119        Declaration::Window(n) => {
120            n.leading_trivia = leading;
121            n.trailing_trivia = trailing;
122        }
123        Declaration::Pix(n) => {
124            n.leading_trivia = leading;
125            n.trailing_trivia = trailing;
126        }
127        Declaration::Ledger(n) => {
128            n.leading_trivia = leading;
129            n.trailing_trivia = trailing;
130        }
131        Declaration::Psyche(n) => {
132            n.leading_trivia = leading;
133            n.trailing_trivia = trailing;
134        }
135        Declaration::Corpus(n) => {
136            n.leading_trivia = leading;
137            n.trailing_trivia = trailing;
138        }
139        Declaration::Dataspace(n) => {
140            n.leading_trivia = leading;
141            n.trailing_trivia = trailing;
142        }
143        Declaration::Ots(n) => {
144            n.leading_trivia = leading;
145            n.trailing_trivia = trailing;
146        }
147        Declaration::Mandate(n) => {
148            n.leading_trivia = leading;
149            n.trailing_trivia = trailing;
150        }
151        Declaration::Compute(n) => {
152            n.leading_trivia = leading;
153            n.trailing_trivia = trailing;
154        }
155        Declaration::Daemon(n) => {
156            n.leading_trivia = leading;
157            n.trailing_trivia = trailing;
158        }
159        Declaration::Extension(n) => {
160            n.leading_trivia = leading;
161            n.trailing_trivia = trailing;
162        }
163        Declaration::AxonStore(n) => {
164            n.leading_trivia = leading;
165            n.trailing_trivia = trailing;
166        }
167        Declaration::AxonEndpoint(n) => {
168            n.leading_trivia = leading;
169            n.trailing_trivia = trailing;
170        }
171        Declaration::Resource(n) => {
172            n.leading_trivia = leading;
173            n.trailing_trivia = trailing;
174        }
175        Declaration::Fabric(n) => {
176            n.leading_trivia = leading;
177            n.trailing_trivia = trailing;
178        }
179        Declaration::Manifest(n) => {
180            n.leading_trivia = leading;
181            n.trailing_trivia = trailing;
182        }
183        Declaration::Observe(n) => {
184            n.leading_trivia = leading;
185            n.trailing_trivia = trailing;
186        }
187        Declaration::Reconcile(n) => {
188            n.leading_trivia = leading;
189            n.trailing_trivia = trailing;
190        }
191        Declaration::Lease(n) => {
192            n.leading_trivia = leading;
193            n.trailing_trivia = trailing;
194        }
195        Declaration::Ensemble(n) => {
196            n.leading_trivia = leading;
197            n.trailing_trivia = trailing;
198        }
199        Declaration::Session(n) => {
200            n.leading_trivia = leading;
201            n.trailing_trivia = trailing;
202        }
203        Declaration::Topology(n) => {
204            n.leading_trivia = leading;
205            n.trailing_trivia = trailing;
206        }
207        Declaration::Immune(n) => {
208            n.leading_trivia = leading;
209            n.trailing_trivia = trailing;
210        }
211        Declaration::Reflex(n) => {
212            n.leading_trivia = leading;
213            n.trailing_trivia = trailing;
214        }
215        Declaration::Heal(n) => {
216            n.leading_trivia = leading;
217            n.trailing_trivia = trailing;
218        }
219        Declaration::Component(n) => {
220            n.leading_trivia = leading;
221            n.trailing_trivia = trailing;
222        }
223        Declaration::View(n) => {
224            n.leading_trivia = leading;
225            n.trailing_trivia = trailing;
226        }
227        Declaration::Channel(n) => {
228            n.leading_trivia = leading;
229            n.trailing_trivia = trailing;
230        }
231        Declaration::Socket(n) => {
232            n.leading_trivia = leading;
233            n.trailing_trivia = trailing;
234        }
235        Declaration::Upstream(n) => {
236            n.leading_trivia = leading;
237            n.trailing_trivia = trailing;
238        }
239        Declaration::Voice(n) => {
240            n.leading_trivia = leading;
241            n.trailing_trivia = trailing;
242        }
243        Declaration::Cors(n) => {
244            n.leading_trivia = leading;
245            n.trailing_trivia = trailing;
246        }
247        Declaration::Credential(n) => {
248            n.leading_trivia = leading;
249            n.trailing_trivia = trailing;
250        }
251        Declaration::Cache(n) => {
252            n.leading_trivia = leading;
253            n.trailing_trivia = trailing;
254        }
255        Declaration::Savant(n) => {
256            n.leading_trivia = leading;
257            n.trailing_trivia = trailing;
258        }
259        Declaration::Synth(n) => {
260            n.leading_trivia = leading;
261            n.trailing_trivia = trailing;
262        }
263        Declaration::Scope(n) => {
264            n.leading_trivia = leading;
265            n.trailing_trivia = trailing;
266        }
267        Declaration::Observable(n) => {
268            n.leading_trivia = leading;
269            n.trailing_trivia = trailing;
270        }
271        Declaration::Witness(n) => {
272            n.leading_trivia = leading;
273            n.trailing_trivia = trailing;
274        }
275        Declaration::Document(n) => {
276            n.leading_trivia = leading;
277            n.trailing_trivia = trailing;
278        }
279        Declaration::Deliver(n) => {
280            n.leading_trivia = leading;
281            n.trailing_trivia = trailing;
282        }
283        Declaration::Notify(n) => {
284            n.leading_trivia = leading;
285            n.trailing_trivia = trailing;
286        }
287        Declaration::Generic(n) => {
288            n.leading_trivia = leading;
289            n.trailing_trivia = trailing;
290        }
291    }
292}
293
294// ── Public error type ────────────────────────────────────────────────────────
295
296/// §Fase 28.d — Source-context constants. D4 ratified 2026-05-10:
297/// 2 lines before + 2 lines after the error line. Mirror of the
298/// Python-side `_SOURCE_CONTEXT_LINES_BEFORE` / `_AFTER` so the
299/// rustc-style block has identical shape across stacks.
300pub const SOURCE_CONTEXT_LINES_BEFORE: usize = 2;
301pub const SOURCE_CONTEXT_LINES_AFTER: usize = 2;
302
303/// §Fase 28.d — Rustc-style source-context block for a parse error.
304///
305/// Holds a reference to the source text plus the line/column the
306/// error points at. Rendering is lazy — call ``render()`` to format
307/// the block (line numbers + caret + 2 lines before + 2 after).
308///
309/// Pure and deterministic: no ANSI colors, no terminal-width
310/// detection. Output shape is byte-identical to the Python
311/// `SourceSnippet.render()` on the same input — that's the cross-
312/// stack drift gate (28.i).
313#[derive(Debug, Clone)]
314pub struct SourceSnippet {
315    pub source: String,
316    pub line: u32,
317    pub column: u32,
318    pub filename: String,
319    pub context_before: usize,
320    pub context_after: usize,
321}
322
323impl SourceSnippet {
324    /// Construct with the default 2/2 context window.
325    pub fn new(source: String, line: u32, column: u32, filename: String) -> Self {
326        Self {
327            source,
328            line,
329            column,
330            filename,
331            context_before: SOURCE_CONTEXT_LINES_BEFORE,
332            context_after: SOURCE_CONTEXT_LINES_AFTER,
333        }
334    }
335
336    /// Format the snippet as a multi-line rustc-style block.
337    ///
338    /// Empty source → empty string. Out-of-range line → empty
339    /// string. Caret column is clamped to `[1, line_len + 1]`.
340    /// Output shape matches Python `SourceSnippet.render` byte-
341    /// identically per D7.
342    #[must_use]
343    pub fn render(&self) -> String {
344        if self.source.is_empty() || self.line < 1 {
345            return String::new();
346        }
347        let raw: Vec<&str> = self.source.split('\n').collect();
348        // Match Python's str.splitlines() trailing-newline shape:
349        // strip an empty trailing entry produced by a final '\n'.
350        let lines: Vec<&str> = if raw.last() == Some(&"") {
351            raw[..raw.len() - 1].to_vec()
352        } else {
353            raw
354        };
355        if lines.is_empty() || self.line as usize > lines.len() {
356            return String::new();
357        }
358
359        let line_idx = self.line as usize;
360        let start = line_idx.saturating_sub(self.context_before).max(1);
361        let end = (line_idx + self.context_after).min(lines.len());
362
363        let gutter = end.to_string().len();
364        let empty_gutter = " ".repeat(gutter);
365
366        let mut out: Vec<String> = Vec::with_capacity(end - start + 4);
367        out.push(format!(
368            "{empty_gutter} --> {}:{}:{}",
369            self.filename, self.line, self.column
370        ));
371        out.push(format!("{empty_gutter} |"));
372        for n in start..=end {
373            let line_text = lines[n - 1];
374            out.push(format!("{n:>gutter$} | {line_text}", gutter = gutter));
375            if n == line_idx {
376                let line_len = line_text.chars().count();
377                let col = (self.column as usize).clamp(1, line_len + 1);
378                out.push(format!(
379                    "{empty_gutter} | {pad}^",
380                    pad = " ".repeat(col - 1)
381                ));
382            }
383        }
384        out.join("\n")
385    }
386}
387
388#[derive(Debug, Clone, Default)]
389pub struct ParseError {
390    pub message: String,
391    pub line: u32,
392    pub column: u32,
393    /// §Fase 28.d — Optional rustc-style source-context block.
394    /// `None` preserves the legacy single-line shape; populated by
395    /// `Parser::with_source` callers (and by `parse_with_recovery`
396    /// / `parse` when a source has been attached to the parser).
397    /// Existing struct-literal call sites use the `..Default::default()`
398    /// idiom (default = None) to stay terse.
399    pub source_snippet: Option<SourceSnippet>,
400}
401
402impl ParseError {
403    /// §Fase 28.d — Attach a `SourceSnippet` derived from raw source
404    /// text and filename. Returns `self` so the call can be chained
405    /// at the construction site. No-op when `line == 0`. Idempotent.
406    #[must_use]
407    pub fn attach_source(mut self, source: &str, filename: &str) -> Self {
408        if self.line >= 1 {
409            self.source_snippet = Some(SourceSnippet::new(
410                source.to_string(),
411                self.line,
412                self.column,
413                filename.to_string(),
414            ));
415        }
416        self
417    }
418}
419
420impl std::fmt::Display for ParseError {
421    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
422        write!(f, "[line {}:{}] {}", self.line, self.column, self.message)?;
423        if let Some(snippet) = &self.source_snippet {
424            let block = snippet.render();
425            if !block.is_empty() {
426                write!(f, "\n{block}")?;
427            }
428        }
429        Ok(())
430    }
431}
432
433impl std::error::Error for ParseError {}
434
435// ── §Fase 28.c — Public recovery result ──────────────────────────────────────
436//
437// Mirror of Python's `axon.compiler.parser.ParseResult` (Fase 28.b).
438// The rationale, sync semantics, and test contract are documented in
439// `docs/fase/fase_28_adopter_diagnostic_robustness.md`. The Rust frontend
440// must produce structurally identical error lists to the Python parser
441// when handed the same source — that is the cross-stack drift gate
442// (D7 ratified 2026-05-10: byte-identical error lists).
443//
444// `program` holds whatever declarations the parser was able to parse
445// successfully. `errors` holds every recovered error in source order.
446// A clean parse returns `errors.is_empty()`; the existing fail-fast
447// `parse()` API is preserved verbatim per D9.
448
449/// Outcome of `Parser::parse_with_recovery` — partial program plus the
450/// list of every error the parser recovered from. See module docs for
451/// the panic-mode + sync-point recovery semantics.
452#[derive(Debug)]
453pub struct ParseResult {
454    pub program: Program,
455    pub errors: Vec<ParseError>,
456}
457
458impl ParseResult {
459    /// True iff at least one parse error was recovered. Callers that
460    /// want to short-circuit on failure should check this rather than
461    /// relying on `program.declarations.is_empty()` (the parser may
462    /// have salvaged some declarations even with errors present).
463    #[inline]
464    #[must_use]
465    pub fn has_errors(&self) -> bool {
466        !self.errors.is_empty()
467    }
468
469    /// Inverse of `has_errors`. Convenience for the "happy path" check
470    /// in tests + adopter integrations.
471    #[inline]
472    #[must_use]
473    pub fn is_clean(&self) -> bool {
474        self.errors.is_empty()
475    }
476}
477
478/// §Fase 28.c — Top-level declaration keywords used as resync points
479/// during error recovery (D2 ratified 2026-05-10). Mirrors the
480/// `_TOP_LEVEL_DECLARATION_KEYWORDS` frozenset on the Python side.
481///
482/// Distinct from `tokens::is_declaration_keyword` because that helper
483/// is used by the structural declaration counter and intentionally
484/// excludes some grammar-only tokens (Know/Believe/Speculate/Doubt,
485/// Ingest, Ots) that DO begin a top-level declaration in
486/// `parse_declaration` and therefore must be valid sync points.
487///
488/// Adding a new top-level dispatch arm in `parse_declaration` MUST
489/// add the corresponding token here so the recovery walker can
490/// re-sync correctly.
491#[inline]
492const fn is_top_level_decl_kw_for_recovery(tt: &TokenType) -> bool {
493    matches!(
494        tt,
495        TokenType::Import
496            | TokenType::Persona
497            | TokenType::Context
498            | TokenType::Anchor
499            | TokenType::Memory
500            | TokenType::Tool
501            | TokenType::Type
502            | TokenType::Flow
503            | TokenType::Intent
504            | TokenType::Run
505            | TokenType::Let
506            | TokenType::Know
507            | TokenType::Believe
508            | TokenType::Speculate
509            | TokenType::Doubt
510            | TokenType::Lambda
511            | TokenType::Agent
512            | TokenType::Shield
513            | TokenType::Pix
514            | TokenType::Ledger
515            | TokenType::Psyche
516            | TokenType::Corpus
517            | TokenType::Dataspace
518            | TokenType::Ots
519            | TokenType::Mandate
520            | TokenType::Compute
521            | TokenType::Daemon
522            // §Fase 87.a/d — the autonomous research primitive + synth policy.
523            | TokenType::Savant
524            | TokenType::Synth
525            // §Fase 88.a — the authorization-scope policy declaration.
526            | TokenType::Scope
527            | TokenType::AxonStore
528            | TokenType::AxonEndpoint
529            | TokenType::Resource
530            | TokenType::Fabric
531            | TokenType::Manifest
532            | TokenType::Observe
533            | TokenType::Reconcile
534            | TokenType::Lease
535            | TokenType::Ensemble
536            | TokenType::Session
537            | TokenType::Topology
538            | TokenType::Immune
539            | TokenType::Reflex
540            | TokenType::Heal
541            | TokenType::Component
542            | TokenType::View
543            | TokenType::Channel
544            | TokenType::Ingest
545            | TokenType::Persist
546            | TokenType::Retrieve
547            | TokenType::Mutate
548            | TokenType::Purge
549            | TokenType::Transact
550            | TokenType::Mcp
551    )
552}
553
554// ── §Fase 30.b — axonendpoint transport + keepalive closed enums ────────────
555//
556// D2 ratified 2026-05-10: `transport` is a closed enum
557// {json, sse, ndjson}. D6 ratified: `keepalive` is a closed enum
558// {5s, 15s, 30s, 60s}. Both mirror the Python frontend's
559// `_AXONENDPOINT_TRANSPORT_VALUES` / `_AXONENDPOINT_KEEPALIVE_VALUES`
560// frozensets in `axon/compiler/parser.py`. Cross-stack drift gate
561// (30.b fixture) asserts byte-identical parse for every entry.
562
563/// Adopter-facing acceptable values for `transport:` field.
564/// Used by both the parser (validation + smart-suggest) and the
565/// type-checker (30.c) so adopter tooling sees one canonical list.
566pub const AXONENDPOINT_TRANSPORT_VALUES: &[&str] = &["json", "sse", "ndjson"];
567
568/// §Fase 33.z.k.b (v1.28.0) — Closed-catalog SSE wire-format
569/// dialects. Selected via the parametrized grammar
570/// `transport: sse(<dialect>)`; bare `transport: sse` resolves to
571/// the Q1 default per the flow's algebraic-effect predicate
572/// (openai for tool-streaming flows; axon for type-annotation-only).
573///
574/// Vertical-grounded scope (Q3 revised 2026-05-14): five dialects
575/// cover ~99% of LLM-streaming adopter expectations.
576///   - `axon`      — current W3C named events
577///                   (event: axon.token / event: axon.complete).
578///                   D6 backwards-compat baseline; indefinitely
579///                   supported as a first-class option.
580///   - `openai`    — `data: {"choices":[{"delta":{...}}]}` frames
581///                   terminated by `data: [DONE]`. OpenAI Chat
582///                   Completions streaming wire verbatim.
583///   - `kimi`      — Moonshot Kimi (kimi.moonshot.cn) — uses the
584///                   OpenAI-compatible Chat Completions wire format
585///                   verbatim (same chunk shape, same `data: [DONE]`
586///                   sentinel). First-class entry so adopters
587///                   declare intent explicitly; under the hood the
588///                   wire is identical to `openai`.
589///   - `glm`       — Zhipu ChatGLM (open.bigmodel.cn) — same as
590///                   kimi, uses OpenAI-compat wire. First-class
591///                   entry for adopter clarity.
592///   - `anthropic` — `event: content_block_delta` frames terminated
593///                   by `event: message_stop`. Adopter SDKs
594///                   targeting Anthropic Claude consume this shape
595///                   verbatim.
596///
597/// Why kimi + glm as first-class entries (Q3 revision rationale):
598/// Bemarking AI's primary adopter pipelines through Kimi K2.x +
599/// Zhipu GLM-4.x. While the wire IS byte-identical to OpenAI's
600/// Chat Completions streaming, declaring `transport: sse(kimi)` /
601/// `transport: sse(glm)` lets the audit trail + observability
602/// surfaces correlate adopter intent against the underlying
603/// provider — without the adopter having to know that "kimi
604/// happens to be OpenAI-compat on the wire today". The runtime
605/// dispatches kimi + glm to the same `OpenAIDialectAdapter` so
606/// the wire shape stays canonical-OpenAI-bytes.
607///
608/// Open-set adapter pluggability (downstream crates registering
609/// custom dialects) remains explicitly out of scope per the
610/// Axon-for-Axon discipline.
611pub const AXONENDPOINT_TRANSPORT_DIALECTS: &[&str] =
612    &["axon", "openai", "kimi", "glm", "anthropic"];
613
614/// Adopter-facing acceptable values for `keepalive:` field.
615pub const AXONENDPOINT_KEEPALIVE_VALUES: &[&str] = &["5s", "15s", "30s", "60s"];
616
617/// §Fase 32.b D3 — Closed method enum for `method:` field. Adopter-
618/// declarable methods only; HEAD/OPTIONS/CONNECT/TRACE are
619/// runtime-managed (CORS preflight, etc.) and never declared from
620/// source. Closed enum refuses interpretation drift; smart-suggest
621/// catches near-misses at parse time.
622///
623/// §Fase 107.a — `QUERY` (RFC 10008, Proposed Standard, June 2026): the safe +
624/// idempotent + cacheable method that CARRIES A REQUEST BODY — the first new HTTP
625/// method in two decades. It carries a LAW, not just a route: `axon-T927` refuses
626/// at compile time a QUERY endpoint whose flow performs a declared write (the
627/// RFC's normative "safe and idempotent" MUST, made a proof).
628///
629/// Must stay in lockstep with `type_checker::VALID_ENDPOINT_METHODS`.
630pub const AXONENDPOINT_METHOD_VALUES: &[&str] =
631    &["GET", "POST", "PUT", "DELETE", "PATCH", "QUERY"];
632
633/// §Fase 36.d (D2) — Closed catalog for the `axonendpoint backend:`
634/// declaration. The set is `CANONICAL_PROVIDERS ∪ {auto, stub}`:
635///
636///   - the seven canonical LLM providers — `anthropic`, `gemini`,
637///     `glm`, `kimi`, `ollama`, `openai`, `openrouter` — a concrete,
638///     declared backend that rung 2 of the Fase 36 D1 resolution
639///     ladder fires immediately;
640///   - `auto` — transparent: declaring it is equivalent to omitting
641///     `backend:` entirely (the route resolves down the ladder —
642///     server default → environment-available providers);
643///   - `stub` — the no-op backend, reachable ONLY by an explicit,
644///     written declaration (D5: a silent degradation to `stub` is
645///     forbidden; an explicit opt-in is not).
646///
647/// `axon-frontend` carries zero runtime deps and therefore cannot
648/// import `axon::backends::CANONICAL_PROVIDERS`; this list is a
649/// hand-maintained mirror. The axon-rs drift gate
650/// (`tests/fase36_d_backend_catalog_drift.rs`) asserts the two stay
651/// byte-identical — adding a provider in one place without the other
652/// fails CI.
653pub const AXONENDPOINT_BACKEND_VALUES: &[&str] = &[
654    "anthropic",
655    "auto",
656    "gemini",
657    "glm",
658    "kimi",
659    "ollama",
660    "openai",
661    "openrouter",
662    "stub",
663];
664
665#[inline]
666fn axonendpoint_is_valid_transport(s: &str) -> bool {
667    AXONENDPOINT_TRANSPORT_VALUES.iter().any(|&v| v == s)
668}
669
670#[inline]
671fn axonendpoint_is_valid_method(s: &str) -> bool {
672    AXONENDPOINT_METHOD_VALUES.iter().any(|&v| v == s)
673}
674
675#[inline]
676fn axonendpoint_is_valid_backend(s: &str) -> bool {
677    AXONENDPOINT_BACKEND_VALUES.iter().any(|&v| v == s)
678}
679
680#[inline]
681fn axonendpoint_is_valid_keepalive(s: &str) -> bool {
682    AXONENDPOINT_KEEPALIVE_VALUES.iter().any(|&v| v == s)
683}
684
685/// §Fase 37.y (D2) — Closed type catalog for query parameters.
686///
687/// Query values arrive over HTTP as URL-encoded strings; the catalog
688/// is the set of types axon will validate / coerce them into for the
689/// Request Binding Contract. Hand-curated, intentionally small:
690///   - `Text` — the raw string (always succeeds)
691///   - `Int` — `i64` parseable
692///   - `Float` — `f64` parseable, finite
693///   - `Bool` — case-insensitive `{true, false, 1, 0, yes, no, on, off}`
694///   - `Uuid` — RFC 4122 textual form
695///
696/// Extending the catalog is a future axon-T?nn surface; v1.38.5 ships
697/// the 5 types covering ~95% of REST query patterns. Lists / dates /
698/// datetimes / enums are honest deferrals (see §7 of the plan vivo).
699pub const AXONENDPOINT_QUERY_PARAM_TYPES: &[&str] =
700    &["Text", "Int", "Float", "Bool", "Uuid"];
701
702/// `true` iff `s` is one of the §Fase 37.y (D2) query-param catalog
703/// entries — exact case-sensitive match (axon types are PascalCase).
704#[inline]
705pub(crate) fn axonendpoint_is_valid_query_param_type(s: &str) -> bool {
706    AXONENDPOINT_QUERY_PARAM_TYPES.iter().any(|&v| v == s)
707}
708
709/// §Fase 37.y (D1) — Extract `{name}` placeholder names from an
710/// `axonendpoint` `path:` string, in left-to-right declaration order.
711///
712/// Recognized placeholder grammar (single-segment, no nested braces):
713/// `{NAME}` where `NAME` matches `[A-Za-z_][A-Za-z0-9_]*`. Anything
714/// inside braces that does NOT match the identifier shape is silently
715/// IGNORED — it's either an adopter typo (caught later by axum at
716/// route registration) or a literal brace in the URL pattern.
717///
718/// Returns `Err(duplicate_name)` when the same `{name}` appears more
719/// than once in the path — HTTP route patterns reject duplicates
720/// structurally (`axum` would panic at registration), so surfacing
721/// the error at parse time is the right place.
722///
723/// Pure + total: never panics; deterministic over its single string
724/// argument. Hand-rolled scanner (no regex dep at parser layer).
725///
726/// # Examples
727///
728/// - `"/api/users"` → `Ok(vec![])`
729/// - `"/api/users/{id}"` → `Ok(vec!["id"])`
730/// - `"/api/tenants/{tenant_id}/secrets/{secret_name}"`
731///   → `Ok(vec!["tenant_id", "secret_name"])`
732/// - `"/api/users/{id}/posts/{id}"` → `Err("id")` (duplicate)
733/// - `"/api/{not valid}"` → `Ok(vec![])` (malformed brace content
734///   silently ignored; axum surfaces the error at registration)
735pub(crate) fn extract_path_param_names(path: &str) -> Result<Vec<String>, String> {
736    let mut out: Vec<String> = Vec::new();
737    let bytes = path.as_bytes();
738    let mut i = 0;
739    while i < bytes.len() {
740        if bytes[i] != b'{' {
741            i += 1;
742            continue;
743        }
744        // Find the matching close brace; if none, the open brace is
745        // a literal — leave it alone.
746        let start = i + 1;
747        let mut end = start;
748        while end < bytes.len() && bytes[end] != b'}' {
749            end += 1;
750        }
751        if end == bytes.len() {
752            // Unterminated — give up; downstream parser/runtime
753            // surface the malformed path elsewhere.
754            break;
755        }
756        let raw = &path[start..end];
757        // Validate identifier shape: [A-Za-z_][A-Za-z0-9_]*
758        let valid = !raw.is_empty()
759            && raw.bytes().enumerate().all(|(idx, b)| {
760                if idx == 0 {
761                    b.is_ascii_alphabetic() || b == b'_'
762                } else {
763                    b.is_ascii_alphanumeric() || b == b'_'
764                }
765            });
766        if valid {
767            let name = raw.to_string();
768            if out.iter().any(|existing| existing == &name) {
769                return Err(name);
770            }
771            out.push(name);
772        }
773        i = end + 1;
774    }
775    Ok(out)
776}
777
778/// §Fase 32.g (D8) — Closed capability-slug grammar. Validates a
779/// `requires:` slug per `^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$`.
780///
781/// Hand-rolled (no regex dep at parser layer) — each segment must
782/// match `[a-z][a-z0-9_]*` and segments are joined by single dots.
783/// Public so the runtime mirror (`axon::auth_scope`) reuses the same
784/// predicate without duplicating the rule.
785///
786/// Examples valid: `admin`, `legal.read`, `hipaa.phi.read`,
787/// `bank.officer.senior`, `a`, `a_b`, `a1`.
788/// Examples invalid: empty, `Admin` (uppercase), `1admin` (digit
789/// first), `bank-officer` (hyphen), `bank..a` (empty segment),
790/// `.admin`, `admin.`, `admin..` .
791pub fn is_valid_capability_slug(slug: &str) -> bool {
792    if slug.is_empty() {
793        return false;
794    }
795    for segment in slug.split('.') {
796        if !is_valid_slug_segment(segment) {
797            return false;
798        }
799    }
800    true
801}
802
803fn is_valid_slug_segment(seg: &str) -> bool {
804    let mut chars = seg.chars();
805    let first = match chars.next() {
806        Some(c) => c,
807        None => return false,
808    };
809    if !first.is_ascii_lowercase() {
810        return false;
811    }
812    chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
813}
814
815// ════════════════════════════════════════════════════════════════════
816//  §Fase 37.y (D1) — `extract_path_param_names` unit tests
817// ════════════════════════════════════════════════════════════════════
818
819// ════════════════════════════════════════════════════════════════════
820//  §Fase 37.y (D2) — `axonendpoint_is_valid_query_param_type` + the
821//  inline `query: { … }` parser, end-to-end through the lexer.
822// ════════════════════════════════════════════════════════════════════
823
824#[cfg(test)]
825mod query_param_catalog_tests {
826    use super::{axonendpoint_is_valid_query_param_type, AXONENDPOINT_QUERY_PARAM_TYPES};
827
828    #[test]
829    fn accepts_every_catalog_entry() {
830        for ty in AXONENDPOINT_QUERY_PARAM_TYPES {
831            assert!(
832                axonendpoint_is_valid_query_param_type(ty),
833                "catalog entry `{ty}` must validate"
834            );
835        }
836    }
837
838    #[test]
839    fn rejects_off_catalog_types() {
840        for off in &[
841            "Timestamp",    // not in v1.38.5 — list/dates deferred
842            "Date",
843            "DateTime",
844            "List<Text>",   // multi-value query params deferred (§7)
845            "Jsonb",        // store-only types not query-applicable
846            "Bytea",
847            "text",         // lowercase rejected (axon types are PascalCase)
848            "TEXT",
849            "Number",       // not in axon's type catalog at all
850            "",             // empty
851            " ",            // whitespace
852        ] {
853            assert!(
854                !axonendpoint_is_valid_query_param_type(off),
855                "off-catalog `{off}` must reject"
856            );
857        }
858    }
859
860    #[test]
861    fn catalog_size_matches_design() {
862        // The plan vivo D2 states a closed 5-type catalog. A future
863        // axon-T?nn surface may extend it; that requires updating BOTH
864        // the catalog AND the plan vivo §7 honest-scope note.
865        assert_eq!(AXONENDPOINT_QUERY_PARAM_TYPES.len(), 5);
866    }
867}
868
869#[cfg(test)]
870mod query_param_parser_tests {
871    use crate::lexer::Lexer;
872    use crate::parser::Parser;
873
874    fn parse_endpoint_source(src: &str) -> Result<crate::ast::AxonEndpointDefinition, String> {
875        let tokens = Lexer::new(src, "test.axon")
876            .tokenize()
877            .map_err(|e| format!("lex: {}", e.message))?;
878        let mut parser = Parser::new(tokens);
879        let program = parser.parse().map_err(|e| format!("parse: {}", e.message))?;
880        program
881            .declarations
882            .into_iter()
883            .find_map(|d| match d {
884                crate::ast::Declaration::AxonEndpoint(e) => Some(e),
885                _ => None,
886            })
887            .ok_or_else(|| "no axonendpoint in program".to_string())
888    }
889
890    #[test]
891    fn endpoint_with_no_query_block_keeps_empty_vec() {
892        let src = r#"
893            axonendpoint write_secret {
894                method: POST
895                path: "/api/users"
896                body: SecretWriteRequest
897                execute: WriteSecret
898            }
899        "#;
900        let ep = parse_endpoint_source(src).expect("parses");
901        assert!(
902            ep.query_params.is_empty(),
903            "D5 — no `query:` block ⇒ empty query_params"
904        );
905    }
906
907    #[test]
908    fn single_query_param_required() {
909        let src = r#"
910            axonendpoint list_users {
911                method: GET
912                path: "/api/users"
913                query: { status: Text }
914                execute: ListUsers
915            }
916        "#;
917        let ep = parse_endpoint_source(src).expect("parses");
918        assert_eq!(ep.query_params.len(), 1);
919        assert_eq!(ep.query_params[0].name, "status");
920        assert_eq!(ep.query_params[0].type_expr.name, "Text");
921        assert!(!ep.query_params[0].type_expr.optional);
922    }
923
924    #[test]
925    fn optional_query_param_via_question_suffix() {
926        let src = r#"
927            axonendpoint list_users {
928                method: GET
929                path: "/api/users"
930                query: { limit: Int? }
931                execute: ListUsers
932            }
933        "#;
934        let ep = parse_endpoint_source(src).expect("parses");
935        assert_eq!(ep.query_params.len(), 1);
936        assert_eq!(ep.query_params[0].name, "limit");
937        assert_eq!(ep.query_params[0].type_expr.name, "Int");
938        assert!(
939            ep.query_params[0].type_expr.optional,
940            "`?` suffix sets optional"
941        );
942    }
943
944    #[test]
945    fn multiple_query_params_preserve_declaration_order() {
946        let src = r#"
947            axonendpoint search {
948                method: GET
949                path: "/api/search"
950                query: { q: Text, page: Int?, limit: Int?, exact: Bool? }
951                execute: Search
952            }
953        "#;
954        let ep = parse_endpoint_source(src).expect("parses");
955        let names: Vec<&str> = ep.query_params.iter().map(|f| f.name.as_str()).collect();
956        assert_eq!(names, vec!["q", "page", "limit", "exact"]);
957        let types: Vec<&str> = ep
958            .query_params
959            .iter()
960            .map(|f| f.type_expr.name.as_str())
961            .collect();
962        assert_eq!(types, vec!["Text", "Int", "Int", "Bool"]);
963        let optionals: Vec<bool> = ep
964            .query_params
965            .iter()
966            .map(|f| f.type_expr.optional)
967            .collect();
968        assert_eq!(optionals, vec![false, true, true, true]);
969    }
970
971    #[test]
972    fn duplicate_query_param_is_parse_error() {
973        let src = r#"
974            axonendpoint bad {
975                method: GET
976                path: "/api/x"
977                query: { name: Text, name: Int? }
978                execute: Bad
979            }
980        "#;
981        let err = parse_endpoint_source(src).expect_err("must fail");
982        assert!(
983            err.contains("duplicate query param 'name'"),
984            "error must name the duplicate. Got: {err}"
985        );
986    }
987
988    #[test]
989    fn off_catalog_type_with_smart_suggest_hint() {
990        // `Strng` is one edit away from `Text` (would suggest `Text`?
991        // Actually edit distance to `Text` is 4; to `Int` is 5. Likely
992        // no smart suggestion within distance 2. The error still names
993        // the catalog explicitly.)
994        let src = r#"
995            axonendpoint bad {
996                method: GET
997                path: "/api/x"
998                query: { value: Strng }
999                execute: Bad
1000            }
1001        "#;
1002        let err = parse_endpoint_source(src).expect_err("must fail");
1003        assert!(
1004            err.contains("unsupported type 'Strng'"),
1005            "error must name the bad type. Got: {err}"
1006        );
1007        assert!(
1008            err.contains("Expected one of: Text | Int | Float | Bool | Uuid"),
1009            "error must list the closed catalog. Got: {err}"
1010        );
1011    }
1012
1013    #[test]
1014    fn close_typo_gets_did_you_mean_hint() {
1015        // `Txt` → edit distance 1 from `Text` → smart-suggest should
1016        // surface the hint.
1017        let src = r#"
1018            axonendpoint bad {
1019                method: GET
1020                path: "/api/x"
1021                query: { value: Txt }
1022                execute: Bad
1023            }
1024        "#;
1025        let err = parse_endpoint_source(src).expect_err("must fail");
1026        assert!(
1027            err.contains("Did you mean") && err.contains("`Text`"),
1028            "smart-suggest must hint `Text`. Got: {err}"
1029        );
1030    }
1031
1032    #[test]
1033    fn every_catalog_type_parses_cleanly() {
1034        // Round-trip smoke for all 5 catalog entries.
1035        for ty in &["Text", "Int", "Float", "Bool", "Uuid"] {
1036            let src = format!(
1037                r#"
1038                    axonendpoint x {{
1039                        method: GET
1040                        path: "/api/x"
1041                        query: {{ v: {ty} }}
1042                        execute: X
1043                    }}
1044                "#
1045            );
1046            let ep = parse_endpoint_source(&src)
1047                .unwrap_or_else(|e| panic!("`{ty}` should parse: {e}"));
1048            assert_eq!(ep.query_params[0].type_expr.name, *ty);
1049        }
1050    }
1051
1052    #[test]
1053    fn comma_optional_between_params() {
1054        // The plan vivo design accepts both comma-separated and
1055        // whitespace-separated query params (existing parser style is
1056        // forgiving). Whitespace-only:
1057        let src = r#"
1058            axonendpoint x {
1059                method: GET
1060                path: "/api/x"
1061                query: { a: Text b: Int? }
1062                execute: X
1063            }
1064        "#;
1065        let ep = parse_endpoint_source(src).expect("parses without commas");
1066        assert_eq!(ep.query_params.len(), 2);
1067    }
1068
1069    // ─── Robustness hardening (37.y.2 100% robust closure) ──────────
1070
1071    #[test]
1072    fn double_query_block_is_parse_error() {
1073        // An adopter who copy-pastes the `query:` block twice should
1074        // see a clear parse error, not a silent merge that produces
1075        // an unexpectedly-augmented endpoint with both blocks fused.
1076        let src = r#"
1077            axonendpoint x {
1078                method: GET
1079                path: "/api/x"
1080                query: { a: Text }
1081                query: { b: Int? }
1082                execute: X
1083            }
1084        "#;
1085        let err = parse_endpoint_source(src).expect_err("must fail");
1086        assert!(
1087            err.contains("declares `query: { … }` more than once"),
1088            "error must call out the duplicate block. Got: {err}"
1089        );
1090        assert!(
1091            err.contains("combine all params into a single block"),
1092            "error must hint the canonical fix. Got: {err}"
1093        );
1094    }
1095
1096    #[test]
1097    fn optional_generic_type_is_parse_error_with_canonical_hint() {
1098        // `Optional<Text>` is the wrong way to declare an optional
1099        // query param. The canonical syntax is `Text?` (the `?`
1100        // suffix). The error must surface this with a literal example.
1101        let src = r#"
1102            axonendpoint x {
1103                method: GET
1104                path: "/api/x"
1105                query: { value: Optional<Text> }
1106                execute: X
1107            }
1108        "#;
1109        let err = parse_endpoint_source(src).expect_err("must fail");
1110        assert!(
1111            err.contains("generic type `Optional<Text>`"),
1112            "error must name the generic type literally. Got: {err}"
1113        );
1114        assert!(
1115            err.contains("Use `Text?` (the `?` suffix)"),
1116            "error must hint the canonical `Text?` syntax. Got: {err}"
1117        );
1118    }
1119
1120    #[test]
1121    fn list_generic_type_is_parse_error_with_deferral_hint() {
1122        // Multi-value query params (`?tag=a&tag=b`) are honest-
1123        // deferred per the plan vivo §7. Adopters who write
1124        // `List<Text>` should see a clear error explaining the
1125        // deferral, not a confusing "type `List` not in catalog".
1126        let src = r#"
1127            axonendpoint x {
1128                method: GET
1129                path: "/api/x"
1130                query: { tags: List<Text> }
1131                execute: X
1132            }
1133        "#;
1134        let err = parse_endpoint_source(src).expect_err("must fail");
1135        assert!(
1136            err.contains("generic type `List<Text>`"),
1137            "error must name the generic type. Got: {err}"
1138        );
1139        assert!(
1140            err.contains("Multi-value query params")
1141                && err.contains("honest-deferred"),
1142            "error must mention the multi-value deferral. Got: {err}"
1143        );
1144    }
1145
1146    #[test]
1147    fn other_generic_types_caught_generically() {
1148        // Generic types beyond `Optional` and `List` get the
1149        // generic-rejection message without a canonical-syntax hint
1150        // (the catalog list is the canonical guidance).
1151        let src = r#"
1152            axonendpoint x {
1153                method: GET
1154                path: "/api/x"
1155                query: { value: Stream<Int> }
1156                execute: X
1157            }
1158        "#;
1159        let err = parse_endpoint_source(src).expect_err("must fail");
1160        assert!(
1161            err.contains("generic type `Stream<Int>`"),
1162            "error must name the generic type. Got: {err}"
1163        );
1164        assert!(
1165            err.contains("Text | Int | Float | Bool | Uuid"),
1166            "error must list the closed catalog. Got: {err}"
1167        );
1168    }
1169
1170    #[test]
1171    fn uuid_optional_parses_cleanly() {
1172        // Hardening companion — `Uuid?` is in the catalog AND
1173        // optional. The two features compose without surprise.
1174        let src = r#"
1175            axonendpoint find {
1176                method: GET
1177                path: "/api/x"
1178                query: { after: Uuid? }
1179                execute: Find
1180            }
1181        "#;
1182        let ep = parse_endpoint_source(src).expect("parses");
1183        assert_eq!(ep.query_params.len(), 1);
1184        assert_eq!(ep.query_params[0].name, "after");
1185        assert_eq!(ep.query_params[0].type_expr.name, "Uuid");
1186        assert!(ep.query_params[0].type_expr.optional);
1187        assert_eq!(ep.query_params[0].type_expr.generic_param, "");
1188    }
1189
1190    #[test]
1191    fn empty_query_block_yields_empty_vec() {
1192        // `query: { }` is grammatically valid but semantically a
1193        // no-op (equivalent to omitting the block). Don't error;
1194        // just record an empty Vec.
1195        let src = r#"
1196            axonendpoint x {
1197                method: GET
1198                path: "/api/x"
1199                query: { }
1200                execute: X
1201            }
1202        "#;
1203        let ep = parse_endpoint_source(src).expect("empty block parses");
1204        assert!(ep.query_params.is_empty());
1205    }
1206
1207    #[test]
1208    fn kivi_secret_write_path_plus_query() {
1209        // Combined path-param + query-param test: an endpoint that
1210        // takes IDs in the URL AND optional filters in the query
1211        // string. This is the natural REST shape Fase 37.y serves.
1212        let src = r#"
1213            axonendpoint write_secret {
1214                method: POST
1215                path: "/api/tenants/{tenant_id}/secrets/{secret_name}"
1216                query: { dry_run: Bool?, overwrite: Bool? }
1217                body: SecretWriteRequest
1218                execute: WriteSecret
1219            }
1220        "#;
1221        let ep = parse_endpoint_source(src).expect("parses");
1222        // Path params populated (from 37.y.1):
1223        assert_eq!(ep.path_params, vec!["tenant_id", "secret_name"]);
1224        // Query params populated (from this sub-fase 37.y.2):
1225        assert_eq!(ep.query_params.len(), 2);
1226        assert_eq!(ep.query_params[0].name, "dry_run");
1227        assert_eq!(ep.query_params[0].type_expr.name, "Bool");
1228        assert!(ep.query_params[0].type_expr.optional);
1229        assert_eq!(ep.query_params[1].name, "overwrite");
1230        // Body still works:
1231        assert_eq!(ep.body_type, "SecretWriteRequest");
1232    }
1233}
1234
1235#[cfg(test)]
1236mod path_param_extraction_tests {
1237    use super::extract_path_param_names;
1238
1239    #[test]
1240    fn empty_path_no_placeholders() {
1241        assert_eq!(extract_path_param_names("/api/users"), Ok(vec![]));
1242        assert_eq!(extract_path_param_names("/"), Ok(vec![]));
1243        assert_eq!(extract_path_param_names(""), Ok(vec![]));
1244    }
1245
1246    #[test]
1247    fn single_placeholder() {
1248        assert_eq!(
1249            extract_path_param_names("/api/users/{id}"),
1250            Ok(vec!["id".to_string()])
1251        );
1252    }
1253
1254    #[test]
1255    fn multiple_placeholders_in_declaration_order() {
1256        assert_eq!(
1257            extract_path_param_names(
1258                "/api/tenants/{tenant_id}/secrets/{secret_name}"
1259            ),
1260            Ok(vec![
1261                "tenant_id".to_string(),
1262                "secret_name".to_string(),
1263            ])
1264        );
1265    }
1266
1267    #[test]
1268    fn kivi_chat_history_path_pattern() {
1269        // The exact pattern the kivi adopter reported (2026-05-20):
1270        // POST /api/tenants/{tenant_id}/secrets/{secret_name}
1271        // Both names extracted in source order.
1272        let names = extract_path_param_names(
1273            "/api/tenants/{tenant_id}/secrets/{secret_name}",
1274        );
1275        assert_eq!(
1276            names,
1277            Ok(vec![
1278                "tenant_id".to_string(),
1279                "secret_name".to_string(),
1280            ])
1281        );
1282    }
1283
1284    #[test]
1285    fn duplicate_placeholder_returns_err() {
1286        assert_eq!(
1287            extract_path_param_names("/api/users/{id}/posts/{id}"),
1288            Err("id".to_string())
1289        );
1290    }
1291
1292    #[test]
1293    fn underscore_and_numeric_in_name() {
1294        assert_eq!(
1295            extract_path_param_names("/api/{user_id}/items/{item_2}"),
1296            Ok(vec!["user_id".to_string(), "item_2".to_string()])
1297        );
1298    }
1299
1300    #[test]
1301    fn leading_underscore_accepted() {
1302        // Identifiers in HTTP paths often start with letters but the
1303        // grammar permits leading underscore (parity with Rust identifier
1304        // rules). The flow parameter name on the binding side has to
1305        // match exactly, so adopters with `_internal_id` in the path
1306        // can pair it with a same-named flow param.
1307        assert_eq!(
1308            extract_path_param_names("/api/{_internal}"),
1309            Ok(vec!["_internal".to_string()])
1310        );
1311    }
1312
1313    #[test]
1314    fn malformed_placeholder_silently_ignored() {
1315        // Content inside `{...}` that does not match the identifier
1316        // grammar is skipped at this layer. axum surfaces the route
1317        // registration failure if the literal text is invalid.
1318        assert_eq!(
1319            extract_path_param_names("/api/{not valid}"),
1320            Ok(vec![])
1321        );
1322        // Empty braces — same: skip silently.
1323        assert_eq!(extract_path_param_names("/api/{}"), Ok(vec![]));
1324        // Mixed: malformed brace skipped, valid placeholder kept.
1325        assert_eq!(
1326            extract_path_param_names("/api/{tenant id}/users/{id}"),
1327            Ok(vec!["id".to_string()])
1328        );
1329    }
1330
1331    #[test]
1332    fn unterminated_brace_returns_clean() {
1333        // Open brace with no close brace — give up without panicking.
1334        // (axum surfaces the malformed-route error at registration.)
1335        assert_eq!(extract_path_param_names("/api/{id"), Ok(vec![]));
1336    }
1337
1338    #[test]
1339    fn placeholders_at_path_boundaries() {
1340        // Placeholder as the very first segment AND the very last
1341        // segment — both should be extracted.
1342        assert_eq!(
1343            extract_path_param_names("{prefix}/api/users/{id}"),
1344            Ok(vec!["prefix".to_string(), "id".to_string()])
1345        );
1346        assert_eq!(
1347            extract_path_param_names("/api/{id}"),
1348            Ok(vec!["id".to_string()])
1349        );
1350    }
1351
1352    #[test]
1353    fn deduplication_detects_non_adjacent_duplicates() {
1354        // The duplicate-detection sweep is global, not just adjacent.
1355        assert_eq!(
1356            extract_path_param_names(
1357                "/api/orgs/{org_id}/teams/{team_id}/repos/{org_id}"
1358            ),
1359            Err("org_id".to_string())
1360        );
1361    }
1362
1363    #[test]
1364    fn never_panics_on_arbitrary_input() {
1365        // Light fuzz: a handful of weird inputs return cleanly.
1366        for input in &[
1367            "{",
1368            "}",
1369            "{}",
1370            "{{}}",
1371            "{{{",
1372            "/api/{}/{id}",
1373            "////",
1374            "\u{1F4A1}",        // emoji (lightbulb)
1375            "\u{0000}",         // null byte
1376        ] {
1377            let _ = extract_path_param_names(input); // must not panic
1378        }
1379    }
1380}
1381
1382#[cfg(test)]
1383mod capability_slug_tests {
1384    use super::is_valid_capability_slug;
1385
1386    #[test]
1387    fn accepts_canonical_examples() {
1388        assert!(is_valid_capability_slug("admin"));
1389        assert!(is_valid_capability_slug("legal.read"));
1390        assert!(is_valid_capability_slug("hipaa.phi.read"));
1391        assert!(is_valid_capability_slug("bank.officer.senior"));
1392        assert!(is_valid_capability_slug("a"));
1393        assert!(is_valid_capability_slug("a_b"));
1394        assert!(is_valid_capability_slug("a1"));
1395        assert!(is_valid_capability_slug("a.b1_c"));
1396    }
1397
1398    #[test]
1399    fn rejects_empty_string() {
1400        assert!(!is_valid_capability_slug(""));
1401    }
1402
1403    #[test]
1404    fn rejects_uppercase() {
1405        assert!(!is_valid_capability_slug("Admin"));
1406        assert!(!is_valid_capability_slug("admin.READ"));
1407    }
1408
1409    #[test]
1410    fn rejects_digit_first() {
1411        assert!(!is_valid_capability_slug("1admin"));
1412        assert!(!is_valid_capability_slug("admin.1read"));
1413    }
1414
1415    #[test]
1416    fn rejects_hyphen() {
1417        assert!(!is_valid_capability_slug("bank-officer"));
1418    }
1419
1420    #[test]
1421    fn rejects_empty_segments() {
1422        assert!(!is_valid_capability_slug("bank..a"));
1423        assert!(!is_valid_capability_slug(".admin"));
1424        assert!(!is_valid_capability_slug("admin."));
1425    }
1426
1427    #[test]
1428    fn rejects_special_chars() {
1429        assert!(!is_valid_capability_slug("admin@read"));
1430        assert!(!is_valid_capability_slug("admin/read"));
1431        assert!(!is_valid_capability_slug("admin read"));
1432    }
1433}
1434
1435// ── Parser ───────────────────────────────────────────────────────────────────
1436
1437pub struct Parser {
1438    tokens: Vec<Token>,
1439    pos: usize,
1440    /// Fase 14.a — leading trivia parallel array, indexed by the
1441    /// effective-token position. `leading_trivia[i]` is the comment
1442    /// trivia that appeared between the previous effective token (or
1443    /// file start) and `tokens[i]`.
1444    leading_trivia: Vec<Vec<Trivia>>,
1445    /// Fase 14.a — trailing trivia parallel array. `trailing_trivia[i]`
1446    /// is the comment trivia on the same line as `tokens[i]`, before
1447    /// the next effective token. Populated by the constructor.
1448    trailing_trivia: Vec<Vec<Trivia>>,
1449    /// Fase 17.a — side-channel for tagging let value_kind. Set by
1450    /// `parse_let_atom` / `parse_let_value_expr` as they descend; read
1451    /// at the end of `parse_let` and stored on the LetStatement.
1452    last_let_value_kind: String,
1453    /// Fase 19.e — loop nesting depth for break/continue scope check.
1454    /// Incremented at the start of `parse_for_in`, decremented after.
1455    /// `parse_break`/`parse_continue` raise ParseError when this is
1456    /// zero (the keyword has no meaning outside a loop body).
1457    loop_depth: u32,
1458    /// §Fase 28.d — Optional source text + filename for the rustc-
1459    /// style source-context block on `ParseError`. Set via the
1460    /// fluent `Parser::with_source` builder; default `None` keeps
1461    /// existing callers (`Parser::new(tokens).parse()`) emitting
1462    /// the legacy single-line shape.
1463    source: Option<String>,
1464    filename: String,
1465}
1466
1467impl Parser {
1468    pub fn new(raw_tokens: Vec<Token>) -> Self {
1469        // ── Fase 14.a — split the raw token stream into:
1470        //   - effective tokens the grammar consumes (cursor advances
1471        //     over these as before),
1472        //   - parallel `leading_trivia` / `trailing_trivia` arrays
1473        //     indexed by effective-token position.
1474        // Comments on a fresh line attach as leading trivia of the
1475        // next effective token; comments on the same line as an
1476        // effective token attach as trailing trivia of that token.
1477        // Roslyn/Swift convention.
1478        let mut effective: Vec<Token> = Vec::with_capacity(raw_tokens.len());
1479        let mut leading: Vec<Vec<Trivia>> = Vec::with_capacity(raw_tokens.len());
1480        let mut trailing: Vec<Vec<Trivia>> = Vec::with_capacity(raw_tokens.len());
1481
1482        let mut pending_leading: Vec<Trivia> = Vec::new();
1483        let mut last_effective_line: i64 = -1;
1484        for tok in raw_tokens {
1485            if is_comment_token(&tok.ttype) {
1486                let kind = token_to_trivia_kind(&tok.ttype)
1487                    .expect("comment token must map to a trivia kind");
1488                let triv = Trivia {
1489                    kind,
1490                    text: tok.value,
1491                    line: tok.line,
1492                    column: tok.column,
1493                };
1494                if !effective.is_empty() && (tok.line as i64) == last_effective_line {
1495                    trailing.last_mut().unwrap().push(triv);
1496                } else {
1497                    pending_leading.push(triv);
1498                }
1499            } else {
1500                last_effective_line = tok.line as i64;
1501                effective.push(tok);
1502                leading.push(std::mem::take(&mut pending_leading));
1503                trailing.push(Vec::new());
1504            }
1505        }
1506
1507        Parser {
1508            tokens: effective,
1509            pos: 0,
1510            leading_trivia: leading,
1511            trailing_trivia: trailing,
1512            last_let_value_kind: "literal".to_string(),
1513            loop_depth: 0,
1514            source: None,
1515            filename: "<source>".to_string(),
1516        }
1517    }
1518
1519    /// §Fase 28.d — Fluent attach of source text + filename for
1520    /// rustc-style source-context blocks on emitted `ParseError`s.
1521    /// Returns `self` so it chains with `.parse_with_recovery()`:
1522    ///
1523    /// ```ignore
1524    /// let result = Parser::new(tokens)
1525    ///     .with_source(src, "foo.axon")
1526    ///     .parse_with_recovery();
1527    /// ```
1528    ///
1529    /// No-op of any other behaviour — pure metadata attach.
1530    #[must_use]
1531    pub fn with_source(mut self, source: &str, filename: &str) -> Self {
1532        self.source = Some(source.to_string());
1533        self.filename = filename.to_string();
1534        self
1535    }
1536
1537    // ── public API ───────────────────────────────────────────────
1538
1539    pub fn parse(&mut self) -> Result<Program, ParseError> {
1540        let mut program = Program {
1541            declarations: Vec::new(),
1542            declaration_trivia: Vec::new(),
1543            loc: Loc { line: 1, column: 1 },
1544        };
1545        while !self.check(TokenType::Eof) {
1546            // Capture trivia around the declaration. `start_pos` is
1547            // the effective-token position of the declaration's first
1548            // token; that position carries the leading trivia. After
1549            // parsing, `pos - 1` is the last token consumed; that
1550            // position carries the trailing trivia.
1551            let start_pos = self.pos;
1552            let mut decl = match self.parse_declaration() {
1553                Ok(d) => d,
1554                Err(e) => return Err(self.attach_source_to_error(e)),
1555            };
1556            let end_pos = self.pos.saturating_sub(1);
1557            let leading = self
1558                .leading_trivia
1559                .get(start_pos)
1560                .cloned()
1561                .unwrap_or_default();
1562            let trailing = self
1563                .trailing_trivia
1564                .get(end_pos)
1565                .cloned()
1566                .unwrap_or_default();
1567            // Fase 14.b — also copy trivia into the per-struct fields on
1568            // the declaration so consumers can read `flow.leading_trivia`
1569            // directly without going through `program.declaration_trivia[i]`.
1570            // The side-channel is preserved for backward compat with
1571            // 14.a callers and as a flat enumeration source.
1572            attach_trivia_to_decl(&mut decl, leading.clone(), trailing.clone());
1573            program.declarations.push(decl);
1574            program
1575                .declaration_trivia
1576                .push(DeclarationTrivia { leading, trailing });
1577        }
1578        // §Fase 80.g — expand `voice` declarations FIRST (they may emit
1579        // `from Preset@vN` upstream legs), then §80.f preset references,
1580        // BEFORE type-check — so the §80.c laws and the IR see the expanded
1581        // program (and `axon desugar` prints exactly this lowering).
1582        // Unknown presets stay unexpanded — the checker reports them with
1583        // the catalog list (accumulating diagnostics beat a parse abort).
1584        crate::voice_desugar::expand(&mut program);
1585        crate::upstream_presets::expand(&mut program);
1586        Ok(program)
1587    }
1588
1589    // ── §Fase 28.c — recovery-mode parse ─────────────────────────
1590    //
1591    // Mirror of Python's `Parser.parse_with_recovery` from
1592    // `axon/compiler/parser.py`. Wraps `parse_declaration` in a
1593    // try/recover loop: on any `ParseError` the error is appended to
1594    // the list and the cursor advances to the next sync point, then
1595    // parsing resumes. The two stacks must produce structurally
1596    // identical error lists on the same input — that is the cross-
1597    // stack drift gate (D7). See the test module
1598    // `tests::fase28_recovery_tests` and Python-side
1599    // `tests/test_fase28_parser_recovery.py`.
1600
1601    /// Recovery-mode parse. Collects every parse error in source
1602    /// order; the existing `parse()` API remains fail-fast (D9).
1603    ///
1604    /// # Recovery contract (D2)
1605    ///
1606    /// On `ParseError`:
1607    ///   1. Push the error onto `errors`.
1608    ///   2. If the cursor is already on a top-level declaration
1609    ///      keyword (and brace-depth ≤ 0), do not consume — the
1610    ///      caller should retry the declaration parse from here.
1611    ///      Otherwise advance one token to make progress, then
1612    ///      walk to the next sync point.
1613    ///   3. Resume the outer loop.
1614    ///
1615    /// Sync points: top-level declaration keyword at brace-depth ≤ 0,
1616    /// or EOF. Negative depths are treated identically to ≤ 0 — the
1617    /// walker keeps walking through over-balanced `}` rather than
1618    /// pretending a closing brace is itself a sync point (which would
1619    /// emit a ghost "Unexpected token at top level" error in the
1620    /// outer loop).
1621    pub fn parse_with_recovery(&mut self) -> ParseResult {
1622        let mut program = Program {
1623            declarations: Vec::new(),
1624            declaration_trivia: Vec::new(),
1625            loc: Loc { line: 1, column: 1 },
1626        };
1627        let mut errors: Vec<ParseError> = Vec::new();
1628
1629        while !self.check(TokenType::Eof) {
1630            let start_pos = self.pos;
1631            match self.parse_declaration() {
1632                Ok(mut decl) => {
1633                    let end_pos = self.pos.saturating_sub(1);
1634                    let leading = self
1635                        .leading_trivia
1636                        .get(start_pos)
1637                        .cloned()
1638                        .unwrap_or_default();
1639                    let trailing = self
1640                        .trailing_trivia
1641                        .get(end_pos)
1642                        .cloned()
1643                        .unwrap_or_default();
1644                    attach_trivia_to_decl(&mut decl, leading.clone(), trailing.clone());
1645                    program.declarations.push(decl);
1646                    program
1647                        .declaration_trivia
1648                        .push(DeclarationTrivia { leading, trailing });
1649                }
1650                Err(err) => {
1651                    // §Fase 28.d — attach source-context block when a
1652                    // source has been provided via `with_source(...)`;
1653                    // otherwise the error keeps its single-line shape.
1654                    errors.push(self.attach_source_to_error(err));
1655                    // Make progress. If parse_declaration returned
1656                    // immediately on the same token (e.g. unknown
1657                    // top-level token), we MUST advance at least one
1658                    // token to avoid an infinite loop.
1659                    if self.pos == start_pos && !self.check(TokenType::Eof) {
1660                        self.advance();
1661                    }
1662                    self.advance_to_sync_point();
1663                }
1664            }
1665        }
1666
1667        ParseResult { program, errors }
1668    }
1669
1670    /// §Fase 28.d — Decorate a `ParseError` with a `SourceSnippet`
1671    /// when the parser has source context attached, otherwise return
1672    /// the error unchanged. Idempotent: if the error already carries
1673    /// a snippet, this overwrites it with the parser's source.
1674    fn attach_source_to_error(&self, err: ParseError) -> ParseError {
1675        match &self.source {
1676            Some(src) if err.line >= 1 => err.attach_source(src, &self.filename),
1677            _ => err,
1678        }
1679    }
1680
1681    /// §Fase 28.c — Walk the cursor forward until the next sync
1682    /// point (top-level declaration keyword at brace-depth ≤ 0) or
1683    /// EOF. Used by `parse_with_recovery` to skip the malformed
1684    /// remainder of a failed declaration.
1685    fn advance_to_sync_point(&mut self) {
1686        let mut depth: i32 = 0;
1687        while !self.check(TokenType::Eof) {
1688            let tt = self.current().ttype.clone();
1689            // Sync at top-level keywords when depth ≤ 0. We do not
1690            // consume the keyword — the outer loop will dispatch on
1691            // it.
1692            if is_top_level_decl_kw_for_recovery(&tt) && depth <= 0 {
1693                return;
1694            }
1695            if matches!(tt, TokenType::LBrace) {
1696                depth += 1;
1697            } else if matches!(tt, TokenType::RBrace) {
1698                depth -= 1;
1699            }
1700            self.advance();
1701        }
1702    }
1703
1704    // ── token helpers ────────────────────────────────────────────
1705
1706    fn current(&self) -> &Token {
1707        if self.pos >= self.tokens.len() {
1708            self.tokens.last().unwrap() // EOF sentinel
1709        } else {
1710            &self.tokens[self.pos]
1711        }
1712    }
1713
1714    fn advance(&mut self) -> &Token {
1715        let idx = self.pos;
1716        if self.pos < self.tokens.len() {
1717            self.pos += 1;
1718        }
1719        &self.tokens[idx]
1720    }
1721
1722    fn check(&self, tt: TokenType) -> bool {
1723        self.current().ttype == tt
1724    }
1725
1726    fn consume(&mut self, expected: TokenType) -> Result<Token, ParseError> {
1727        let tok = self.current().clone();
1728        if tok.ttype != expected {
1729            return Err(ParseError {
1730                message: format!(
1731                    "Expected {:?}, found {:?}('{}')",
1732                    expected, tok.ttype, tok.value
1733                ),
1734                line: tok.line,
1735                column: tok.column,
1736                            ..Default::default()
1737            });
1738        }
1739        self.pos += 1;
1740        Ok(tok)
1741    }
1742
1743    /// §Fase 41.b — build a `ParseError` at the current token's location.
1744    fn error(&self, message: &str) -> ParseError {
1745        let tok = self.current();
1746        ParseError { message: message.to_string(), line: tok.line, column: tok.column, ..Default::default() }
1747    }
1748
1749    /// Consume any identifier or keyword-used-as-value.
1750    fn consume_any_ident_or_kw(&mut self) -> Result<Token, ParseError> {
1751        let tok = self.current().clone();
1752        match tok.ttype {
1753            TokenType::Identifier
1754            | TokenType::Bool
1755            | TokenType::StringLit
1756            | TokenType::Integer
1757            | TokenType::Float => {
1758                self.pos += 1;
1759                Ok(tok)
1760            }
1761            _ => {
1762                // Allow any keyword token whose value is alphabetic
1763                if !tok.value.is_empty()
1764                    && tok.value.chars().all(|c| c.is_alphanumeric() || c == '_')
1765                    && tok.ttype != TokenType::Eof
1766                {
1767                    self.pos += 1;
1768                    Ok(tok)
1769                } else {
1770                    Err(ParseError {
1771                        message: format!(
1772                            "Expected identifier or keyword value, found {:?}('{}')",
1773                            tok.ttype, tok.value
1774                        ),
1775                        line: tok.line,
1776                        column: tok.column,
1777                                            ..Default::default()
1778                    })
1779                }
1780            }
1781        }
1782    }
1783
1784    fn consume_number(&mut self) -> Result<f64, ParseError> {
1785        let tok = self.current().clone();
1786        match tok.ttype {
1787            TokenType::Float | TokenType::Integer => {
1788                self.pos += 1;
1789                tok.value.parse::<f64>().map_err(|_| ParseError {
1790                    message: format!("Invalid number '{}'", tok.value),
1791                    line: tok.line,
1792                    column: tok.column,
1793                                    ..Default::default()
1794                })
1795            }
1796            _ => Err(ParseError {
1797                message: format!("Expected number, found {:?}('{}')", tok.ttype, tok.value),
1798                line: tok.line,
1799                column: tok.column,
1800                            ..Default::default()
1801            }),
1802        }
1803    }
1804
1805    fn parse_bool(&mut self) -> Result<bool, ParseError> {
1806        let tok = self.consume(TokenType::Bool)?;
1807        Ok(tok.value == "true")
1808    }
1809
1810    fn loc_of(&self, tok: &Token) -> Loc {
1811        Loc {
1812            line: tok.line,
1813            column: tok.column,
1814        }
1815    }
1816
1817    fn check_comparison(&self) -> bool {
1818        matches!(
1819            self.current().ttype,
1820            TokenType::Lt
1821                | TokenType::Gt
1822                | TokenType::Lte
1823                | TokenType::Gte
1824                | TokenType::Eq
1825                | TokenType::Neq
1826        )
1827    }
1828
1829    fn check_run_modifier(&self) -> bool {
1830        matches!(
1831            self.current().ttype,
1832            TokenType::As
1833                | TokenType::Within
1834                | TokenType::ConstrainedBy
1835                | TokenType::OnFailure
1836                | TokenType::OutputTo
1837                | TokenType::Effort
1838        )
1839    }
1840
1841    // ── list helpers ─────────────────────────────────────────────
1842
1843    fn parse_string_list(&mut self) -> Result<Vec<String>, ParseError> {
1844        self.consume(TokenType::LBracket)?;
1845        let mut items = Vec::new();
1846        items.push(self.consume(TokenType::StringLit)?.value);
1847        while self.check(TokenType::Comma) {
1848            self.advance();
1849            items.push(self.consume(TokenType::StringLit)?.value);
1850        }
1851        self.consume(TokenType::RBracket)?;
1852        Ok(items)
1853    }
1854
1855    /// §Fase 83.a — a bracketed list of quoted string literals, tolerant of
1856    /// an empty `[]` and a trailing comma before `]` (the `Window.exclude`
1857    /// shape, generalized into a reusable helper). Used for CORS field
1858    /// lists whose values contain characters (`://`, `.`, `-`) that aren't
1859    /// valid bare identifiers — `allow_origins`, `allow_headers`,
1860    /// `expose_headers` — where `parse_string_list`'s "at least one item,
1861    /// no trailing comma" strictness would reject a legitimate empty or
1862    /// comma-terminated declaration.
1863    fn parse_bracketed_strings(&mut self) -> Result<Vec<String>, ParseError> {
1864        self.consume(TokenType::LBracket)?;
1865        let mut items = Vec::new();
1866        if !self.check(TokenType::RBracket) {
1867            items.push(self.consume(TokenType::StringLit)?.value);
1868            while self.check(TokenType::Comma) {
1869                self.advance();
1870                if self.check(TokenType::RBracket) {
1871                    break; // trailing comma
1872                }
1873                items.push(self.consume(TokenType::StringLit)?.value);
1874            }
1875        }
1876        self.consume(TokenType::RBracket)?;
1877        Ok(items)
1878    }
1879
1880    fn parse_identifier_list(&mut self) -> Result<Vec<String>, ParseError> {
1881        let mut names = Vec::new();
1882        names.push(self.consume(TokenType::Identifier)?.value);
1883        while self.check(TokenType::Comma) {
1884            self.advance();
1885            names.push(self.consume(TokenType::Identifier)?.value);
1886        }
1887        Ok(names)
1888    }
1889
1890    fn parse_bracketed_identifiers(&mut self) -> Result<Vec<String>, ParseError> {
1891        self.consume(TokenType::LBracket)?;
1892        let items = self.parse_extended_identifier_list()?;
1893        self.consume(TokenType::RBracket)?;
1894        Ok(items)
1895    }
1896
1897    fn parse_extended_identifier_list(&mut self) -> Result<Vec<String>, ParseError> {
1898        let mut items = Vec::new();
1899        items.push(self.consume_any_ident_or_kw()?.value);
1900        while self.check(TokenType::Comma) {
1901            self.advance();
1902            items.push(self.consume_any_ident_or_kw()?.value);
1903        }
1904        Ok(items)
1905    }
1906
1907    fn parse_dotted_identifier(&mut self) -> Result<String, ParseError> {
1908        let mut parts = vec![self.consume_any_ident_or_kw()?.value];
1909        while self.check(TokenType::Dot) {
1910            self.advance();
1911            parts.push(self.consume_any_ident_or_kw()?.value);
1912        }
1913        Ok(parts.join("."))
1914    }
1915
1916    fn parse_expression_string(&mut self) -> Result<String, ParseError> {
1917        if self.check(TokenType::LBracket) {
1918            let items = self.parse_bracketed_dot_identifiers()?;
1919            return Ok(format!("[{}]", items.join(", ")));
1920        }
1921        self.parse_dotted_identifier()
1922    }
1923
1924    fn parse_bracketed_dot_identifiers(&mut self) -> Result<Vec<String>, ParseError> {
1925        self.consume(TokenType::LBracket)?;
1926        let mut items = vec![self.parse_dotted_identifier()?];
1927        while self.check(TokenType::Comma) {
1928            self.advance();
1929            items.push(self.parse_dotted_identifier()?);
1930        }
1931        self.consume(TokenType::RBracket)?;
1932        Ok(items)
1933    }
1934
1935    fn parse_argument_list(&mut self) -> Result<Vec<String>, ParseError> {
1936        let mut args = Vec::new();
1937        while !self.check(TokenType::RParen) {
1938            let tok = self.current().clone();
1939            match tok.ttype {
1940                TokenType::StringLit | TokenType::Integer | TokenType::Float => {
1941                    self.advance();
1942                    args.push(tok.value);
1943                }
1944                TokenType::Identifier => {
1945                    self.advance();
1946                    let mut val = tok.value;
1947                    if self.check(TokenType::Dot) {
1948                        self.advance();
1949                        val.push('.');
1950                        val.push_str(&self.consume_any_ident_or_kw()?.value);
1951                    }
1952                    args.push(val);
1953                }
1954                _ => {
1955                    self.advance();
1956                    let key = tok.value;
1957                    if self.check(TokenType::Colon) {
1958                        self.advance();
1959                        let v = self.advance().value.clone();
1960                        args.push(format!("{key}:{v}"));
1961                    } else {
1962                        args.push(key);
1963                    }
1964                }
1965            }
1966            if self.check(TokenType::Comma) {
1967                self.advance();
1968            }
1969        }
1970        Ok(args)
1971    }
1972
1973    /// Skip a single value or balanced bracketed/braced block (unknown field).
1974    fn skip_value(&mut self) {
1975        match self.current().ttype {
1976            TokenType::LBracket => {
1977                self.advance();
1978                let mut depth = 1u32;
1979                while depth > 0 && !self.check(TokenType::Eof) {
1980                    if self.check(TokenType::LBracket) {
1981                        depth += 1;
1982                    } else if self.check(TokenType::RBracket) {
1983                        depth -= 1;
1984                    }
1985                    self.advance();
1986                }
1987            }
1988            TokenType::LBrace => {
1989                self.advance();
1990                let mut depth = 1u32;
1991                while depth > 0 && !self.check(TokenType::Eof) {
1992                    if self.check(TokenType::LBrace) {
1993                        depth += 1;
1994                    } else if self.check(TokenType::RBrace) {
1995                        depth -= 1;
1996                    }
1997                    self.advance();
1998                }
1999            }
2000            TokenType::Lt => {
2001                // effect row: <io, network, ...>
2002                self.advance();
2003                let mut depth = 1u32;
2004                while depth > 0 && !self.check(TokenType::Eof) {
2005                    if self.check(TokenType::Lt) {
2006                        depth += 1;
2007                    } else if self.check(TokenType::Gt) {
2008                        depth -= 1;
2009                    }
2010                    self.advance();
2011                }
2012            }
2013            _ => {
2014                self.advance();
2015                while self.check(TokenType::Dot) {
2016                    self.advance();
2017                    self.advance();
2018                }
2019            }
2020        }
2021    }
2022
2023    /// Skip a balanced `{ ... }` block including its braces.
2024    fn skip_braced_block(&mut self) -> Result<(), ParseError> {
2025        self.consume(TokenType::LBrace)?;
2026        let mut depth = 1u32;
2027        while depth > 0 {
2028            if self.check(TokenType::Eof) {
2029                let tok = self.current();
2030                return Err(ParseError {
2031                    message: "Unterminated block — expected '}'".to_string(),
2032                    line: tok.line,
2033                    column: tok.column,
2034                                    ..Default::default()
2035                });
2036            }
2037            if self.check(TokenType::LBrace) {
2038                depth += 1;
2039            } else if self.check(TokenType::RBrace) {
2040                depth -= 1;
2041            }
2042            self.advance();
2043        }
2044        Ok(())
2045    }
2046
2047    fn at_declaration_start(&self) -> bool {
2048        is_declaration_keyword(&self.current().ttype) || self.check(TokenType::Eof)
2049    }
2050
2051    // ── top-level dispatch ───────────────────────────────────────
2052
2053    fn parse_declaration(&mut self) -> Result<Declaration, ParseError> {
2054        let tok = self.current().clone();
2055
2056        // §Fase 114.a — a TOP-LEVEL `budget <Name> { … }`.
2057        //
2058        // `budget` lexes as `TokenType::Budget` (the daemon-field keyword). At top
2059        // level it is only a declaration when a NAME follows — `budget Foo { … }`.
2060        // The lookahead is what keeps the daemon-attached form (`daemon D { budget
2061        // { … } }`, where `{` follows immediately) untouched: there the next token
2062        // is `{`, not an identifier, so this branch does not fire.
2063        if tok.ttype == TokenType::Budget && self.peek_is_identifier() {
2064            return self.parse_top_level_budget().map(Declaration::Budget);
2065        }
2066
2067        match tok.ttype {
2068            TokenType::Import => self.parse_import().map(Declaration::Import),
2069            TokenType::Persona => self.parse_persona().map(Declaration::Persona),
2070            TokenType::Context => self.parse_context().map(Declaration::Context),
2071            TokenType::Anchor => self.parse_anchor().map(Declaration::Anchor),
2072            TokenType::Memory => self.parse_memory().map(Declaration::Memory),
2073            TokenType::Tool => self.parse_tool().map(Declaration::Tool),
2074            TokenType::Type => self.parse_type_def().map(Declaration::Type),
2075            TokenType::Flow => self.parse_flow().map(Declaration::Flow),
2076            TokenType::Intent => self.parse_intent().map(Declaration::Intent),
2077            TokenType::Run => self.parse_run().map(Declaration::Run),
2078            TokenType::Let => self.parse_let().map(Declaration::Let),
2079            TokenType::Know | TokenType::Believe | TokenType::Speculate | TokenType::Doubt => {
2080                self.parse_epistemic_block().map(Declaration::Epistemic)
2081            }
2082            TokenType::Lambda => self.parse_lambda_data().map(Declaration::LambdaData),
2083
2084            // ── Tier 2 declarations (full AST) ──────────────────
2085            TokenType::Agent => self.parse_agent().map(Declaration::Agent),
2086            TokenType::Shield => self.parse_shield().map(Declaration::Shield),
2087            // §Fase 71.a — temporal execution-window guard.
2088            TokenType::Window => self.parse_window().map(Declaration::Window),
2089            TokenType::Pix => self.parse_pix().map(Declaration::Pix),
2090            TokenType::Ledger => self.parse_ledger().map(Declaration::Ledger),
2091            TokenType::Psyche => self.parse_psyche().map(Declaration::Psyche),
2092            TokenType::Corpus => self.parse_corpus().map(Declaration::Corpus),
2093            TokenType::Dataspace => self.parse_dataspace().map(Declaration::Dataspace),
2094            TokenType::Ots => self.parse_ots().map(Declaration::Ots),
2095            TokenType::Mandate => self.parse_mandate().map(Declaration::Mandate),
2096            TokenType::Compute => self.parse_compute().map(Declaration::Compute),
2097            TokenType::Daemon => self.parse_daemon().map(Declaration::Daemon),
2098            TokenType::Extension => self.parse_extension().map(Declaration::Extension),
2099            TokenType::AxonStore => self.parse_axonstore().map(Declaration::AxonStore),
2100            TokenType::AxonEndpoint => self.parse_axonendpoint().map(Declaration::AxonEndpoint),
2101
2102            // ── §λ-L-E Fase 1 — I/O cognitivo ───────────────────
2103            TokenType::Resource => self.parse_resource().map(Declaration::Resource),
2104            TokenType::Fabric => self.parse_fabric().map(Declaration::Fabric),
2105            TokenType::Manifest => self.parse_manifest().map(Declaration::Manifest),
2106            TokenType::Observe => self.parse_observe().map(Declaration::Observe),
2107
2108            // ── §λ-L-E Fase 3 — Control cognitivo ───────────────
2109            TokenType::Reconcile => self.parse_reconcile().map(Declaration::Reconcile),
2110            TokenType::Lease => self.parse_lease().map(Declaration::Lease),
2111            TokenType::Ensemble => self.parse_ensemble().map(Declaration::Ensemble),
2112
2113            // ── §λ-L-E Fase 4 — Topology + π-calculus sessions ─
2114            TokenType::Session => self.parse_session_definition().map(Declaration::Session),
2115            TokenType::Topology => self.parse_topology().map(Declaration::Topology),
2116
2117            // ── §Fase 41.b — typed WebSocket transport ─────────
2118            TokenType::Socket => self.parse_socket().map(Declaration::Socket),
2119
2120            // ── §Fase 80.b — outbound vendor connection ─────────
2121            TokenType::Upstream => self.parse_upstream().map(Declaration::Upstream),
2122
2123            // ── §Fase 80.g — the voice-agent simplicity layer ───
2124            TokenType::Voice => self.parse_voice().map(Declaration::Voice),
2125
2126            // ── §Fase 83.a — the named origin-policy declaration ─
2127            TokenType::Cors => self.parse_cors().map(Declaration::Cors),
2128
2129            // ── §Fase 85.a — the named result-memoization policy ─
2130            TokenType::Cache => self.parse_cache().map(Declaration::Cache),
2131            TokenType::Document => self.parse_document().map(Declaration::Document),
2132
2133            // ── §Fase 105 — Governed CRM Delivery ─
2134            TokenType::Deliver => self.parse_deliver().map(Declaration::Deliver),
2135            TokenType::Notify => self.parse_notify().map(Declaration::Notify),
2136
2137            // ── §Fase 87.a — the long-horizon autonomous research primitive ─
2138            TokenType::Savant => self.parse_savant().map(Declaration::Savant),
2139
2140            // ── §Fase 87.d — the dynamic tool-synthesis policy ──────────────
2141            TokenType::Synth => self.parse_synth().map(Declaration::Synth),
2142
2143            // ── §Fase 88.a — the authorization-scope policy declaration ─────
2144            TokenType::Scope => self.parse_scope().map(Declaration::Scope),
2145
2146            // ── §Fase 92.a — the ephemeral-credential contract ──────────────
2147            TokenType::Credential => self.parse_credential().map(Declaration::Credential),
2148
2149            // ── §Fase 51.c.2 — Pauli-sum observable ────────────
2150            TokenType::Observable => self.parse_observable().map(Declaration::Observable),
2151
2152            // ── §Fase 69.a — Advantage Witness ──────────────────
2153            TokenType::Witness => self.parse_witness().map(Declaration::Witness),
2154
2155            // ── §λ-L-E Fase 5 — Cognitive immune system ─────────
2156            TokenType::Immune => self.parse_immune().map(Declaration::Immune),
2157            TokenType::Reflex => self.parse_reflex().map(Declaration::Reflex),
2158            TokenType::Heal => self.parse_heal().map(Declaration::Heal),
2159
2160            // ── §λ-L-E Fase 9 — UI cognitiva ────────────────────
2161            TokenType::Component => self.parse_component().map(Declaration::Component),
2162            TokenType::View => self.parse_view().map(Declaration::View),
2163
2164            // ── §λ-L-E Fase 13 — Mobile typed channels ──────────
2165            TokenType::Channel => self.parse_channel().map(Declaration::Channel),
2166
2167            // ── Tier 3+ structural fallback ─────────────────────
2168            // Store operations: keyword target { ... } or keyword target ...
2169            TokenType::Ingest
2170            | TokenType::Persist
2171            | TokenType::Retrieve
2172            | TokenType::Mutate
2173            | TokenType::Purge
2174            | TokenType::Transact => self.parse_generic_declaration(),
2175
2176            // MCP declaration
2177            TokenType::Mcp => self.parse_generic_declaration(),
2178
2179            _ => {
2180                // §Fase 28.e — append "Did you mean X?" hint when the
2181                // unknown token looks like a typo'd top-level keyword
2182                // (Levenshtein ≤ 2). D3, D11 ratified 2026-05-10.
2183                let hint = crate::smart_suggest::suggest_for(
2184                    &tok.value,
2185                    crate::smart_suggest::TOP_LEVEL_KEYWORD_NAMES,
2186                );
2187                let base = format!(
2188                    "Unexpected token at top level: '{}' — expected declaration \
2189                     (persona, context, anchor, flow, run, ...)",
2190                    tok.value
2191                );
2192                let message = if hint.is_empty() {
2193                    base
2194                } else {
2195                    format!("{base}. {hint}")
2196                };
2197                Err(ParseError {
2198                    message,
2199                    line: tok.line,
2200                    column: tok.column,
2201                    ..Default::default()
2202                })
2203            }
2204        }
2205    }
2206
2207    // ── IMPORT ───────────────────────────────────────────────────
2208
2209    fn parse_import(&mut self) -> Result<ImportNode, ParseError> {
2210        let tok = self.consume(TokenType::Import)?;
2211        let loc = self.loc_of(&tok);
2212
2213        let mut path_parts = Vec::new();
2214
2215        // Optional @ scope
2216        if self.check(TokenType::At) {
2217            self.advance();
2218            let first = self.consume(TokenType::Identifier)?;
2219            path_parts.push(format!("@{}", first.value));
2220        } else {
2221            let first = self.consume(TokenType::Identifier)?;
2222            path_parts.push(first.value);
2223        }
2224
2225        while self.check(TokenType::Dot) {
2226            self.advance();
2227            if self.check(TokenType::LBrace) {
2228                break;
2229            }
2230            let part = self.consume(TokenType::Identifier)?;
2231            path_parts.push(part.value);
2232        }
2233
2234        let mut names = Vec::new();
2235        if self.check(TokenType::LBrace) {
2236            self.advance();
2237            names = self.parse_identifier_list()?;
2238            self.consume(TokenType::RBrace)?;
2239        }
2240
2241        // ── §Fase 115.c — the `@allow_downgrade` ECC valve ───────────────
2242        //
2243        // `import a.b.{X} @allow_downgrade` acknowledges an epistemic
2244        // downgrade across this edge (see `epistemic_compat.rs`). The
2245        // annotation position is unambiguous: no top-level declaration
2246        // begins with `@`, so an `@` here belongs to this import — and an
2247        // unknown annotation is refused with the fix in the message
2248        // rather than surfacing later as an opaque parse error.
2249        let mut allow_downgrade = false;
2250        if self.check(TokenType::At) {
2251            let at_tok = self.current().clone();
2252            self.advance();
2253            let ident = self.consume(TokenType::Identifier)?;
2254            if ident.value == "allow_downgrade" {
2255                allow_downgrade = true;
2256            } else {
2257                return Err(ParseError {
2258                    message: format!(
2259                        "unknown import annotation '@{}' — the only import annotation is \
2260                         `@allow_downgrade` (the §115 epistemic-downgrade acknowledgment).",
2261                        ident.value
2262                    ),
2263                    line: at_tok.line,
2264                    column: at_tok.column,
2265                    ..Default::default()
2266                });
2267            }
2268        }
2269
2270        // ── §Fase 111 — `apx` is RETRACTED ───────────────────────────────
2271        //
2272        // `import X with apx { … }` used to parse and then call
2273        // `skip_braced_block()` — the policy was consumed and thrown on the
2274        // floor. It never reached the AST, let alone the IR. In `axon-rs` the
2275        // string "apx" occurred only inside comments: there is no APX crate,
2276        // no binary, no MEC/PCC dependency verification, no EPR ranking, no
2277        // quarantine and no compliance gate. The public README advertised all
2278        // five.
2279        //
2280        // A dependency policy that silently evaporates is the worst possible
2281        // shape for this particular promise: the adopter believes their supply
2282        // chain is being verified, which is exactly the belief that stops them
2283        // from verifying it themselves. Refuse, loudly.
2284        let next_is_apx = self
2285            .tokens
2286            .get(self.pos + 1)
2287            .map(|t| t.value == "apx")
2288            .unwrap_or(false);
2289        if self.current().value == "with" && next_is_apx {
2290            let tok = self.current().clone();
2291            return Err(ParseError {
2292                message: "`import … with apx { … }` is RETRACTED (§111). The apx policy block was \
2293                          parsed and silently DISCARDED — it never reached the IR, and no epistemic \
2294                          dependency manager exists: no MEC/PCC verification, no EPR ranking, no \
2295                          quarantine, no compliance gate. Declaring it verified nothing while \
2296                          implying your supply chain was checked. Remove the `with apx { … }` \
2297                          clause; the plain `import` resolves through the §115 Epistemic Module \
2298                          System."
2299                    .to_string(),
2300                line: tok.line,
2301                column: tok.column,
2302                ..Default::default()
2303            });
2304        }
2305
2306        Ok(ImportNode {
2307            module_path: path_parts,
2308            names,
2309            allow_downgrade,
2310            loc,
2311            leading_trivia: Vec::new(),
2312            trailing_trivia: Vec::new(),
2313        })
2314    }
2315
2316    // ── PERSONA ──────────────────────────────────────────────────
2317
2318    fn parse_persona(&mut self) -> Result<PersonaDefinition, ParseError> {
2319        let tok = self.consume(TokenType::Persona)?;
2320        let loc = self.loc_of(&tok);
2321        let name = self.consume(TokenType::Identifier)?.value;
2322        self.consume(TokenType::LBrace)?;
2323
2324        let mut node = PersonaDefinition {
2325            name,
2326            domain: Vec::new(),
2327            tone: String::new(),
2328            confidence_threshold: None,
2329            cite_sources: None,
2330            refuse_if: Vec::new(),
2331            language: String::new(),
2332            description: String::new(),
2333            loc,
2334            leading_trivia: Vec::new(),
2335            trailing_trivia: Vec::new(),
2336        };
2337
2338        while !self.check(TokenType::RBrace) {
2339            let field_name = self.current().value.clone();
2340            self.advance();
2341            self.consume(TokenType::Colon)?;
2342
2343            match field_name.as_str() {
2344                "domain" => node.domain = self.parse_string_list()?,
2345                "tone" => node.tone = self.consume_any_ident_or_kw()?.value,
2346                "confidence_threshold" => node.confidence_threshold = Some(self.consume_number()?),
2347                "cite_sources" => node.cite_sources = Some(self.parse_bool()?),
2348                "refuse_if" => node.refuse_if = self.parse_bracketed_identifiers()?,
2349                "language" => node.language = self.consume(TokenType::StringLit)?.value,
2350                "description" => node.description = self.consume(TokenType::StringLit)?.value,
2351                _ => self.skip_value(),
2352            }
2353        }
2354        self.consume(TokenType::RBrace)?;
2355        Ok(node)
2356    }
2357
2358    // ── CONTEXT ──────────────────────────────────────────────────
2359
2360    fn parse_context(&mut self) -> Result<ContextDefinition, ParseError> {
2361        let tok = self.consume(TokenType::Context)?;
2362        let loc = self.loc_of(&tok);
2363        let name = self.consume(TokenType::Identifier)?.value;
2364        self.consume(TokenType::LBrace)?;
2365
2366        let mut node = ContextDefinition {
2367            name,
2368            memory_scope: String::new(),
2369            language: String::new(),
2370            depth: String::new(),
2371            max_tokens: None,
2372            temperature: None,
2373            cite_sources: None,
2374            now_tz: None,
2375            loc,
2376            leading_trivia: Vec::new(),
2377            trailing_trivia: Vec::new(),
2378        };
2379
2380        while !self.check(TokenType::RBrace) {
2381            let field_name = self.current().value.clone();
2382            self.advance();
2383            self.consume(TokenType::Colon)?;
2384
2385            match field_name.as_str() {
2386                "memory" => node.memory_scope = self.consume_any_ident_or_kw()?.value,
2387                "language" => node.language = self.consume(TokenType::StringLit)?.value,
2388                "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
2389                // §Fase 91.a — the frame's cognitive timezone (IANA string).
2390                "now" => node.now_tz = Some(self.consume(TokenType::StringLit)?.value),
2391                "max_tokens" => {
2392                    node.max_tokens = Some(
2393                        self.consume(TokenType::Integer)?
2394                            .value
2395                            .parse::<i64>()
2396                            .unwrap_or(0),
2397                    )
2398                }
2399                "temperature" => node.temperature = Some(self.consume_number()?),
2400                "cite_sources" => node.cite_sources = Some(self.parse_bool()?),
2401                _ => self.skip_value(),
2402            }
2403        }
2404        self.consume(TokenType::RBrace)?;
2405        Ok(node)
2406    }
2407
2408    // ── ANCHOR ───────────────────────────────────────────────────
2409
2410    fn parse_anchor(&mut self) -> Result<AnchorConstraint, ParseError> {
2411        let tok = self.consume(TokenType::Anchor)?;
2412        let loc = self.loc_of(&tok);
2413        let name = self.consume(TokenType::Identifier)?.value;
2414        self.consume(TokenType::LBrace)?;
2415
2416        let mut node = AnchorConstraint {
2417            name,
2418            require: String::new(),
2419            reject: Vec::new(),
2420            enforce: String::new(),
2421            description: String::new(),
2422            confidence_floor: None,
2423            unknown_response: String::new(),
2424            on_violation: String::new(),
2425            on_violation_target: String::new(),
2426            loc,
2427            leading_trivia: Vec::new(),
2428            trailing_trivia: Vec::new(),
2429        };
2430
2431        while !self.check(TokenType::RBrace) {
2432            let field_name = self.current().value.clone();
2433            self.advance();
2434            self.consume(TokenType::Colon)?;
2435
2436            match field_name.as_str() {
2437                "require" => node.require = self.consume_any_ident_or_kw()?.value,
2438                "description" => node.description = self.consume(TokenType::StringLit)?.value,
2439                "reject" => node.reject = self.parse_bracketed_identifiers()?,
2440                "enforce" => node.enforce = self.consume_any_ident_or_kw()?.value,
2441                "confidence_floor" => node.confidence_floor = Some(self.consume_number()?),
2442                "unknown_response" => {
2443                    node.unknown_response = self.consume(TokenType::StringLit)?.value
2444                }
2445                "on_violation" => {
2446                    // Parse: raise ErrorName | fallback(...) | identifier
2447                    let action = self.consume_any_ident_or_kw()?.value;
2448                    node.on_violation = action.clone();
2449                    if action == "raise" || action == "fallback" {
2450                        node.on_violation_target = self.consume_any_ident_or_kw()?.value;
2451                    }
2452                }
2453                _ => self.skip_value(),
2454            }
2455        }
2456        self.consume(TokenType::RBrace)?;
2457        Ok(node)
2458    }
2459
2460    // ── MEMORY ───────────────────────────────────────────────────
2461
2462    fn parse_memory(&mut self) -> Result<MemoryDefinition, ParseError> {
2463        let tok = self.consume(TokenType::Memory)?;
2464        let loc = self.loc_of(&tok);
2465        let name = self.consume(TokenType::Identifier)?.value;
2466        self.consume(TokenType::LBrace)?;
2467
2468        let mut node = MemoryDefinition {
2469            name,
2470            store: String::new(),
2471            backend: String::new(),
2472            retrieval: String::new(),
2473            decay: String::new(),
2474            loc,
2475            leading_trivia: Vec::new(),
2476            trailing_trivia: Vec::new(),
2477        };
2478
2479        while !self.check(TokenType::RBrace) {
2480            let field_name = self.current().value.clone();
2481            self.advance();
2482            self.consume(TokenType::Colon)?;
2483
2484            match field_name.as_str() {
2485                "store" => node.store = self.consume_any_ident_or_kw()?.value,
2486                "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
2487                "retrieval" => node.retrieval = self.consume_any_ident_or_kw()?.value,
2488                "decay" => {
2489                    if self.check(TokenType::Duration) {
2490                        node.decay = self.advance().value.clone();
2491                    } else {
2492                        node.decay = self.consume_any_ident_or_kw()?.value;
2493                    }
2494                }
2495                _ => self.skip_value(),
2496            }
2497        }
2498        self.consume(TokenType::RBrace)?;
2499        Ok(node)
2500    }
2501
2502    // ── TOOL ─────────────────────────────────────────────────────
2503
2504    fn parse_tool(&mut self) -> Result<ToolDefinition, ParseError> {
2505        let tok = self.consume(TokenType::Tool)?;
2506        let loc = self.loc_of(&tok);
2507        let name = self.consume(TokenType::Identifier)?.value;
2508        self.consume(TokenType::LBrace)?;
2509
2510        let mut node = ToolDefinition {
2511            name,
2512            provider: String::new(),
2513            max_results: None,
2514            filter_expr: String::new(),
2515            timeout: String::new(),
2516            runtime: String::new(),
2517            resource_ref: String::new(),
2518            sandbox: None,
2519            effects: None,
2520            parameters: Vec::new(),
2521            output_type: None,
2522            secret: String::new(),
2523            secret_partition: String::new(),
2524            target: None,
2525            risk: None,
2526            argv: Vec::new(),
2527            cache: String::new(),
2528            scrape: None,
2529            loc,
2530            leading_trivia: Vec::new(),
2531            trailing_trivia: Vec::new(),
2532        };
2533
2534        // §Fase 84.b/D84.13 — unknown fields are recorded (not silently
2535        // skipped) so a `target:`-bound technician tool can HARD-ERROR on one
2536        // (a typo'd safety field must never quietly disable a guard), while a
2537        // legacy schema-less tool keeps its lenient record-and-skip (zero
2538        // regression). The decision is deferred to after the block is parsed,
2539        // since `target:` may appear after the unknown field.
2540        let mut unknown_fields: Vec<(String, u32, u32)> = Vec::new();
2541
2542        while !self.check(TokenType::RBrace) {
2543            let field_tok = self.current().clone();
2544            let field_name = field_tok.value.clone();
2545            self.advance();
2546            self.consume(TokenType::Colon)?;
2547
2548            match field_name.as_str() {
2549                "provider" => node.provider = self.consume_any_ident_or_kw()?.value,
2550                "max_results" => {
2551                    node.max_results = Some(
2552                        self.consume(TokenType::Integer)?
2553                            .value
2554                            .parse::<i64>()
2555                            .unwrap_or(0),
2556                    )
2557                }
2558                "filter" => node.filter_expr = self.parse_filter_expression()?,
2559                "timeout" => node.timeout = self.consume(TokenType::Duration)?.value,
2560                "runtime" => node.runtime = self.consume_any_ident_or_kw()?.value,
2561                // §Fase 114.c — the `resource` this tool's channel runs on. The
2562                // channel's address, concurrency and lifecycle come from it;
2563                // `runtime:` then names the path within the channel.
2564                "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
2565                "sandbox" => node.sandbox = Some(self.parse_bool()?),
2566                "effects" => node.effects = Some(self.parse_effect_row()?),
2567                // §Fase 58.a — the tool's typed input schema + output type.
2568                "parameters" => node.parameters = self.parse_tool_param_schema()?,
2569                "output_type" => node.output_type = Some(self.parse_output_type_string()?),
2570                // §Fase 94.c — the per-tenant secret KEY injected at
2571                // dispatch (`rotation_without_revelation`). Key shape +
2572                // technician exclusion are `axon-T902` (type-checker).
2573                "secret" => node.secret = self.parse_dotted_identifier()?,
2574                // §Fase 95.a — `secret_partition:` names one of this tool's
2575                // own `parameters:` (a bare identifier, NOT dotted — it is a
2576                // parameter reference, not a key). Its runtime value becomes
2577                // a single appended key segment at dispatch. The membership +
2578                // `String`-type + technician laws are `axon-T903`.
2579                "secret_partition" => {
2580                    node.secret_partition = self.consume_any_ident_or_kw()?.value
2581                }
2582                // §Fase 84.b — Remote Hands technician fields.
2583                "target" => node.target = Some(self.consume_any_ident_or_kw()?.value),
2584                "risk" => node.risk = Some(self.consume_any_ident_or_kw()?.value),
2585                // The argv template: a bracketed list of quoted elements
2586                // (`argv: ["ping", "-c", "${count}", "${host}"]`). Reuses the
2587                // CORS list helper (tolerant of `[]` and a trailing comma).
2588                "argv" => node.argv = self.parse_bracketed_strings()?,
2589                // §Fase 85.b — the tool's result-memoization policy reference
2590                // (a declared `cache` name, or the `none` opt-out sentinel).
2591                "cache" => node.cache = self.consume_any_ident_or_kw()?.value,
2592                // §Fase 98.b — the closed-catalog web-acquisition config
2593                // block. `scrape: { engine: …, extract: […], … }`.
2594                "scrape" => node.scrape = Some(self.parse_scrape_spec()?),
2595                _ => {
2596                    unknown_fields.push((field_name, field_tok.line, field_tok.column));
2597                    self.skip_value();
2598                }
2599            }
2600        }
2601        self.consume(TokenType::RBrace)?;
2602
2603        // §Fase 84.b/D84.13 — a `target:`-bound tool opts into strict field
2604        // checking. An unknown field on it is a parse error, mirroring the §83
2605        // `cors`/`voice` closed-catalog discipline — but scoped to the
2606        // technician surface so ordinary tools are untouched.
2607        // §Fase 98.b (D98.2) — a `scrape:`-bearing web-acquisition tool opts
2608        // into the same strictness: a typo'd safety field (e.g. a mis-spelled
2609        // `respect_robots`) must never quietly disable a guard.
2610        if node.target.is_some() || node.scrape.is_some() {
2611            if let Some((field_name, line, column)) = unknown_fields.into_iter().next() {
2612                let (surface, valid) = if node.target.is_some() {
2613                    (
2614                        "technician tool (§Fase 84 D84.13)",
2615                        "provider, parameters, output_type, timeout, effects, target, risk, argv",
2616                    )
2617                } else {
2618                    (
2619                        "web-acquisition tool (§Fase 98 D98.2)",
2620                        "provider, parameters, output_type, timeout, effects, secret, \
2621                         secret_partition, cache, scrape",
2622                    )
2623                };
2624                return Err(ParseError {
2625                    message: format!(
2626                        "unknown field `{field_name}` in {surface} `{}` — this tool uses \
2627                         strict field checking; valid fields: {valid}",
2628                        node.name
2629                    ),
2630                    line,
2631                    column,
2632                    ..Default::default()
2633                });
2634            }
2635        }
2636        Ok(node)
2637    }
2638
2639    /// §Fase 98.b — parse the closed-catalog `scrape: { … }` web-acquisition
2640    /// config sub-block. Every field is optional; an unknown field is a hard
2641    /// parse error (the §83 `cors` closed-catalog discipline). Mirrors the
2642    /// field grammar of `parse_tool` for the scrape-specific keys.
2643    fn parse_scrape_spec(&mut self) -> Result<crate::ast::ScrapeSpec, ParseError> {
2644        let open = self.consume(TokenType::LBrace)?;
2645        let loc = self.loc_of(&open);
2646        let mut spec = crate::ast::ScrapeSpec {
2647            loc,
2648            ..Default::default()
2649        };
2650        while !self.check(TokenType::RBrace) {
2651            let field_tok = self.current().clone();
2652            let field_name = field_tok.value.clone();
2653            self.advance();
2654            self.consume(TokenType::Colon)?;
2655            match field_name.as_str() {
2656                "engine" => spec.engine = Some(self.consume_any_ident_or_kw()?.value),
2657                "impersonate" => spec.impersonate = Some(self.consume_any_ident_or_kw()?.value),
2658                "render_wait" => spec.render_wait = Some(self.consume(TokenType::Duration)?.value),
2659                "proxy" => spec.proxy = self.parse_dotted_identifier()?,
2660                "respect_robots" => spec.respect_robots = Some(self.parse_bool()?),
2661                "extract" => spec.extract = self.parse_bracketed_strings()?,
2662                "adaptive" => spec.adaptive = Some(self.parse_bool()?),
2663                "similarity_floor" => spec.similarity_floor = self.parse_optional_float(),
2664                "follow" => spec.follow = self.consume(TokenType::StringLit)?.value,
2665                "max_depth" => {
2666                    spec.max_depth =
2667                        Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2668                }
2669                "max_pages" => {
2670                    spec.max_pages =
2671                        Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2672                }
2673                "concurrency" => {
2674                    spec.concurrency =
2675                        Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2676                }
2677                "politeness" => spec.politeness = self.consume_any_ident_or_kw()?.value,
2678                "checkpoint" => spec.checkpoint = self.consume_any_ident_or_kw()?.value,
2679                other => {
2680                    return Err(self.error(&format!(
2681                        "unknown scrape field `{other}` — the `scrape: {{ … }}` block is a \
2682                         closed catalog (§Fase 98 D98.2); valid fields: engine, impersonate, \
2683                         render_wait, proxy, respect_robots, extract, adaptive, \
2684                         similarity_floor, follow, max_depth, max_pages, concurrency, \
2685                         politeness, checkpoint"
2686                    )));
2687                }
2688            }
2689        }
2690        self.consume(TokenType::RBrace)?;
2691        Ok(spec)
2692    }
2693
2694    /// §Fase 58.a — parse a tool's INPUT SCHEMA: a brace-delimited list of
2695    /// `name: Type` parameters (`parameters: { query: String, max_results: Int }`).
2696    /// Reuses the flow-parameter shape (`Parameter`), so the same `TypeExpr`
2697    /// grammar — generics like `List<T>`, `?`-optionals — applies. A trailing
2698    /// comma is tolerated; an empty `{}` yields no parameters.
2699    fn parse_tool_param_schema(&mut self) -> Result<Vec<Parameter>, ParseError> {
2700        self.consume(TokenType::LBrace)?;
2701        let mut params = Vec::new();
2702        while !self.check(TokenType::RBrace) {
2703            // Accept a keyword-as-name (`filter`, `type`, `domain`, …) — real
2704            // adopter tool schemas use such parameter names; the `:` after it
2705            // disambiguates.
2706            let name = self.consume_any_ident_or_kw()?;
2707            let ploc = self.loc_of(&name);
2708            self.consume(TokenType::Colon)?;
2709            let type_expr = self.parse_type_expr()?;
2710            params.push(Parameter {
2711                name: name.value,
2712                type_expr,
2713                loc: ploc,
2714            });
2715            if self.check(TokenType::Comma) {
2716                self.advance();
2717            } else {
2718                break;
2719            }
2720        }
2721        self.consume(TokenType::RBrace)?;
2722        Ok(params)
2723    }
2724
2725    fn parse_filter_expression(&mut self) -> Result<String, ParseError> {
2726        let name = self.consume_any_ident_or_kw()?.value;
2727        if self.check(TokenType::LParen) {
2728            self.advance();
2729            let mut parts = vec![name, "(".to_string()];
2730            while !self.check(TokenType::RParen) {
2731                parts.push(self.advance().value.clone());
2732            }
2733            self.consume(TokenType::RParen)?;
2734            parts.push(")".to_string());
2735            Ok(parts.join(""))
2736        } else {
2737            Ok(name)
2738        }
2739    }
2740
2741    fn parse_effect_row(&mut self) -> Result<EffectRow, ParseError> {
2742        let tok = self.consume(TokenType::Lt)?;
2743        let loc = self.loc_of(&tok);
2744        let mut effects = Vec::new();
2745        let mut epistemic_level = String::new();
2746
2747        while !self.check(TokenType::Gt) {
2748            let name = self.consume_any_ident_or_kw()?.value;
2749            if self.check(TokenType::Colon) {
2750                self.advance();
2751                // Fase 11.c / 11.e — qualifiers can be compound slugs
2752                // from a closed catalogue:
2753                //
2754                //   * dot-separated  — `legal:HIPAA.164_502`,
2755                //                       `legal:GDPR.Art6.Consent`,
2756                //                       `legal:PCI_DSS.v4_Req3`
2757                //   * colon-separated — `ots:transform:mulaw8:pcm16`,
2758                //                       `ots:backend:native`
2759                //   * mixed           — supported by the same loop.
2760                //
2761                // The lexer fragments dotted slugs across IDENT /
2762                // INTEGER tokens (e.g., `164_502` lexes as INTEGER
2763                // `164` + IDENT `_502` because `_` starts a fresh
2764                // identifier); we recombine here using source-column
2765                // adjacency so the type checker sees the catalog
2766                // string verbatim.
2767                let level = self.parse_qualifier_value()?;
2768                if name == "epistemic" {
2769                    epistemic_level = level;
2770                } else {
2771                    effects.push(format!("{name}:{level}"));
2772                }
2773            } else {
2774                effects.push(name);
2775            }
2776            if self.check(TokenType::Comma) {
2777                self.advance();
2778            }
2779        }
2780        self.consume(TokenType::Gt)?;
2781
2782        Ok(EffectRow {
2783            effects,
2784            epistemic_level,
2785            loc,
2786        })
2787    }
2788
2789    /// Parse a compound qualifier value following an effect's first
2790    /// colon — supports both dot-separated (`HIPAA.164_502`) and
2791    /// colon-separated (`transform:mulaw8:pcm16`) catalogue slugs, as
2792    /// well as mixed forms.
2793    ///
2794    /// The grammar is: `segment ((`.` | `:`) segment)*` where a
2795    /// segment is a contiguous run of IDENT / INTEGER tokens (see
2796    /// [`Self::consume_dotted_slug_segment`]).
2797    fn parse_qualifier_value(&mut self) -> Result<String, ParseError> {
2798        let mut buf = self.consume_dotted_slug_segment()?;
2799        loop {
2800            let sep = if self.check(TokenType::Dot) {
2801                '.'
2802            } else if self.check(TokenType::Colon) {
2803                ':'
2804            } else {
2805                break;
2806            };
2807            self.advance();
2808            let part = self.consume_dotted_slug_segment()?;
2809            buf.push(sep);
2810            buf.push_str(&part);
2811        }
2812        Ok(buf)
2813    }
2814
2815    /// Consume a contiguous run of IDENT / INTEGER / keyword-ident
2816    /// tokens whose source positions are adjacent (no whitespace
2817    /// between them), concatenating their text into a single segment.
2818    ///
2819    /// Needed for closed-catalogue qualifier slugs whose segment
2820    /// mixes digits and identifier characters — e.g. `HIPAA.164_502`
2821    /// lexes as INTEGER `164` + IDENT `_502` because `_` starts a
2822    /// fresh identifier; the catalog value is the concatenation
2823    /// `164_502`. Adjacency is determined by matching
2824    /// `(line, column + len)` of the previous token against the next
2825    /// token's start position.
2826    fn consume_dotted_slug_segment(&mut self) -> Result<String, ParseError> {
2827        let first = self.consume_any_ident_or_kw()?;
2828        let mut buf = first.value.clone();
2829        let mut next_line = first.line;
2830        let mut next_col = first.column + first.value.chars().count() as u32;
2831        loop {
2832            let cur = self.current();
2833            let is_segment_token = matches!(cur.ttype, TokenType::Identifier | TokenType::Integer,);
2834            if !is_segment_token {
2835                break;
2836            }
2837            if cur.line != next_line || cur.column != next_col {
2838                break;
2839            }
2840            buf.push_str(&cur.value);
2841            next_col = cur.column + cur.value.chars().count() as u32;
2842            next_line = cur.line;
2843            self.pos += 1;
2844        }
2845        Ok(buf)
2846    }
2847
2848    // ── TYPE ─────────────────────────────────────────────────────
2849
2850    fn parse_type_def(&mut self) -> Result<TypeDefinition, ParseError> {
2851        let tok = self.consume(TokenType::Type)?;
2852        let loc = self.loc_of(&tok);
2853        let name = self.consume(TokenType::Identifier)?.value;
2854
2855        let mut node = TypeDefinition {
2856            name,
2857            fields: Vec::new(),
2858            range_constraint: None,
2859            where_clause: None,
2860            compliance: Vec::new(),
2861            loc: loc.clone(),
2862            leading_trivia: Vec::new(),
2863            trailing_trivia: Vec::new(),
2864        };
2865
2866        // Optional range: (0.0..1.0)
2867        if self.check(TokenType::LParen) {
2868            self.advance();
2869            let min_val = self.consume_number()?;
2870            self.consume(TokenType::DotDot)?;
2871            let max_val = self.consume_number()?;
2872            self.consume(TokenType::RParen)?;
2873            node.range_constraint = Some(RangeConstraint {
2874                min_value: min_val,
2875                max_value: max_val,
2876                loc: loc.clone(),
2877            });
2878        }
2879
2880        // Optional where clause
2881        if self.check(TokenType::Where) {
2882            self.advance();
2883            let mut expr_parts = Vec::new();
2884            while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
2885                if self.check(TokenType::Eof) {
2886                    break;
2887                }
2888                expr_parts.push(self.advance().value.clone());
2889            }
2890            node.where_clause = Some(WhereClause {
2891                expression: expr_parts.join(" "),
2892                loc: loc.clone(),
2893            });
2894        }
2895
2896        // Optional ESK Fase 6.1 — `compliance [HIPAA, ...]` prefix modifier
2897        // between `type Name` / `range` / `where` and the body `{`.
2898        if self.check(TokenType::Identifier) && self.current().value == "compliance" {
2899            self.advance();
2900            node.compliance = self.parse_bracketed_identifiers()?;
2901        }
2902
2903        // Optional body: { field: Type, ... }
2904        if self.check(TokenType::LBrace) {
2905            self.advance();
2906            while !self.check(TokenType::RBrace) {
2907                let field_name = self.consume(TokenType::Identifier)?;
2908                let field_loc = self.loc_of(&field_name);
2909                self.consume(TokenType::Colon)?;
2910                let type_expr = self.parse_type_expr()?;
2911                node.fields.push(TypeField {
2912                    name: field_name.value,
2913                    type_expr,
2914                    loc: field_loc,
2915                });
2916                if self.check(TokenType::Comma) {
2917                    self.advance();
2918                }
2919            }
2920            self.consume(TokenType::RBrace)?;
2921        }
2922
2923        Ok(node)
2924    }
2925
2926    fn parse_type_expr(&mut self) -> Result<TypeExpr, ParseError> {
2927        let name_tok = self.consume(TokenType::Identifier)?;
2928        let loc = self.loc_of(&name_tok);
2929        let mut generic_param = String::new();
2930        let mut optional = false;
2931
2932        if self.check(TokenType::Lt) {
2933            self.advance();
2934            // §Fase 39.a — recursive: the generic param can itself be a
2935            // nested type expression. `FlowEnvelope<List<TenantRecord>>`
2936            // parses as outer=FlowEnvelope, inner=List<TenantRecord>.
2937            // Pre-39.a the inner had to be a single Identifier; nested
2938            // generics like the canonical FlowEnvelope<T> wrapper
2939            // required this lift. Backwards-compat preserved for
2940            // single-level generics like `Stream<Token>` and
2941            // `List<T>` — the recursion lands once and returns the
2942            // same flat string the v1.x parser produced.
2943            let inner = self.parse_type_expr()?;
2944            generic_param = if inner.generic_param.is_empty() {
2945                inner.name
2946            } else {
2947                format!("{}<{}>", inner.name, inner.generic_param)
2948            };
2949            self.consume(TokenType::Gt)?;
2950        }
2951        // §Fase 51.c.3 — bracket type parameters for the continuous-carrier
2952        // grammar: `SymbolicPtr[Tensor[Float32]]`, `DensityMatrix[1024]`. The
2953        // param is either a nested type expression OR a numeric dimension.
2954        if self.check(TokenType::LBracket) {
2955            self.advance();
2956            if matches!(self.current().ttype, TokenType::Integer | TokenType::Float) {
2957                generic_param = self.advance().value.clone();
2958            } else {
2959                let inner = self.parse_type_expr()?;
2960                generic_param = if inner.generic_param.is_empty() {
2961                    inner.name
2962                } else {
2963                    format!("{}[{}]", inner.name, inner.generic_param)
2964                };
2965            }
2966            self.consume(TokenType::RBracket)?;
2967        }
2968        if self.check(TokenType::Question) {
2969            self.advance();
2970            optional = true;
2971        }
2972
2973        Ok(TypeExpr {
2974            name: name_tok.value,
2975            generic_param,
2976            optional,
2977            loc,
2978        })
2979    }
2980
2981    /// Parse a type expression in a context where the AST stores the
2982    /// shape as a flat string (step / reason / forge / ots-apply
2983    /// productions). Mirrors Python `_parse_output_type_string`.
2984    ///
2985    /// Accepts:
2986    /// - `Identifier`        → `"Identifier"`
2987    /// - `Stream<String>`    → `"Stream<String>"`
2988    /// - `Optional?`         → `"Optional?"`
2989    /// - `Stream<String>?`   → `"Stream<String>?"`
2990    ///
2991    /// **Why this exists** — pre-fix, the step parser called
2992    /// `consume(TokenType::Identifier)?.value` which captured only
2993    /// the head identifier and left `< … >` unconsumed. For
2994    /// `output: Stream<Token>`, this produced `output_type =
2995    /// "Stream"`, and downstream `flow_has_stream_output`'s
2996    /// `starts_with("Stream<") && ends_with('>')` predicate then
2997    /// returned false → `implicit_transport == "json"` → the
2998    /// dynamic-route fallback in `axon-rs` served JSON instead of
2999    /// SSE even when the adopter's source canonically declared the
3000    /// algebraic stream effect. Surfaced 2026-05-12 by adopter
3001    /// `docs/MIGRATION_TO_AXON.md` audit after the v1.23.0 wire-
3002    /// layer didn't honor the declarative effect. Python parser was
3003    /// fixed for the same gap 2026-05-09; this is the Rust cross-
3004    /// stack catch-up.
3005    fn parse_output_type_string(&mut self) -> Result<String, ParseError> {
3006        let expr = self.parse_type_expr()?;
3007        let mut s = expr.name;
3008        if !expr.generic_param.is_empty() {
3009            s.push('<');
3010            s.push_str(&expr.generic_param);
3011            s.push('>');
3012        }
3013        if expr.optional {
3014            s.push('?');
3015        }
3016        Ok(s)
3017    }
3018
3019    // ── FLOW ─────────────────────────────────────────────────────
3020
3021    fn parse_flow(&mut self) -> Result<FlowDefinition, ParseError> {
3022        let tok = self.consume(TokenType::Flow)?;
3023        let loc = self.loc_of(&tok);
3024        let name = self.consume(TokenType::Identifier)?.value;
3025
3026        self.consume(TokenType::LParen)?;
3027        let mut parameters = Vec::new();
3028        if !self.check(TokenType::RParen) {
3029            parameters = self.parse_param_list()?;
3030        }
3031        self.consume(TokenType::RParen)?;
3032
3033        let mut return_type = None;
3034        if self.check(TokenType::Arrow) {
3035            self.advance();
3036            return_type = Some(self.parse_type_expr()?);
3037        }
3038
3039        self.consume(TokenType::LBrace)?;
3040        let mut body = Vec::new();
3041        while !self.check(TokenType::RBrace) {
3042            body.push(self.parse_flow_step()?);
3043        }
3044        self.consume(TokenType::RBrace)?;
3045
3046        Ok(FlowDefinition {
3047            name,
3048            parameters,
3049            return_type,
3050            body,
3051            loc,
3052            leading_trivia: Vec::new(),
3053            trailing_trivia: Vec::new(),
3054        })
3055    }
3056
3057    fn parse_param_list(&mut self) -> Result<Vec<Parameter>, ParseError> {
3058        let mut params = Vec::new();
3059
3060        let name = self.consume(TokenType::Identifier)?;
3061        let ploc = self.loc_of(&name);
3062        self.consume(TokenType::Colon)?;
3063        let type_expr = self.parse_type_expr()?;
3064        params.push(Parameter {
3065            name: name.value,
3066            type_expr,
3067            loc: ploc,
3068        });
3069
3070        while self.check(TokenType::Comma) {
3071            self.advance();
3072            let name = self.consume(TokenType::Identifier)?;
3073            let ploc = self.loc_of(&name);
3074            self.consume(TokenType::Colon)?;
3075            let type_expr = self.parse_type_expr()?;
3076            params.push(Parameter {
3077                name: name.value,
3078                type_expr,
3079                loc: ploc,
3080            });
3081        }
3082        Ok(params)
3083    }
3084
3085    // ── FLOW STEP dispatch ───────────────────────────────────────
3086
3087    fn parse_flow_step(&mut self) -> Result<FlowStep, ParseError> {
3088        let tok = self.current().clone();
3089
3090        match tok.ttype {
3091            TokenType::Step => self.parse_step().map(FlowStep::Step),
3092            TokenType::If => self.parse_if().map(FlowStep::If),
3093            TokenType::For => self.parse_for_in().map(FlowStep::ForIn),
3094            TokenType::Let => self.parse_let().map(FlowStep::Let),
3095            TokenType::Return => self.parse_return().map(FlowStep::Return),
3096            TokenType::Break => self.parse_break().map(FlowStep::Break),
3097            TokenType::Continue => self.parse_continue().map(FlowStep::Continue),
3098            TokenType::Lambda => self.parse_lambda_data_apply().map(FlowStep::LambdaDataApply),
3099
3100            // ── Tier 2 flow steps (typed AST) ─────────────────────
3101            TokenType::Probe => self.parse_flow_step_simple("probe").map(|l| FlowStep::Probe(ProbeStep { target: l.1, loc: l.0 })),
3102            TokenType::Reason => self.parse_flow_step_simple("reason").map(|l| FlowStep::Reason(ReasonStep { strategy: String::new(), target: l.1, loc: l.0 })),
3103            TokenType::Validate => self.parse_flow_step_simple("validate").map(|l| FlowStep::Validate(ValidateStep { target: l.1, rule: String::new(), loc: l.0 })),
3104            TokenType::Refine => self.parse_flow_step_simple("refine").map(|l| FlowStep::Refine(RefineStep { target: l.1, strategy: String::new(), loc: l.0 })),
3105            TokenType::Weave => self.parse_weave_step(),
3106            TokenType::Use => self.parse_use_step(),
3107            TokenType::Remember => self.parse_remember_step(),
3108            TokenType::Recall => self.parse_recall_step(),
3109            TokenType::Par => self.parse_par_block().map(FlowStep::Par),
3110            TokenType::Hibernate => self.parse_hibernate_step(),
3111            TokenType::Deliberate => self.parse_block_step("deliberate").map(|l| FlowStep::Deliberate(DeliberateBlock { loc: l })),
3112            TokenType::Consensus => self.parse_block_step("consensus").map(|l| FlowStep::Consensus(ConsensusBlock { loc: l })),
3113            TokenType::Forge => self.parse_forge_step().map(FlowStep::Forge),
3114            TokenType::Focus => self.parse_focus_step(),
3115            TokenType::Grad => self.parse_grad_step(),
3116            TokenType::Associate => self.parse_associate_step(),
3117            TokenType::Aggregate => self.parse_aggregate_step(),
3118            TokenType::Explore => self.parse_explore_step(),
3119            TokenType::Ingest => self.parse_ingest_step(),
3120            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 })),
3121            // §Fase 111.e — `stream` parses its BODY. It used to go through
3122            // `parse_block_step`, whose entire job is `skip_braced_block()` —
3123            // the block's contents were thrown away at parse time, which is why
3124            // `run_stream` had nothing to run and "completed" with an empty
3125            // string while the README sold "Algebraic Effects and Free Monads".
3126            TokenType::Stream => self.parse_stream_block().map(FlowStep::Stream),
3127            TokenType::Navigate => self.parse_navigate_step(),
3128            TokenType::Drill => self.parse_drill_step(),
3129            TokenType::Trail => self.parse_flow_step_simple("trail").map(|l| FlowStep::Trail(TrailStep { navigate_ref: l.1, loc: l.0 })),
3130            TokenType::Corroborate => self.parse_corroborate_step(),
3131            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 })),
3132            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 })),
3133            // §Fase 111.f — `compute <Name> on a, b -> out`. The ARGUMENTS used to
3134            // be `Vec::new()` — hardcoded empty at the parse site — so even if
3135            // the runtime had wanted to compute something, it had nothing to
3136            // compute it FROM.
3137            TokenType::Compute => self.parse_compute_apply().map(FlowStep::ComputeApply),
3138            TokenType::Listen => self.parse_listen_step(),
3139            TokenType::Daemon => self.parse_flow_step_simple("daemon").map(|l| FlowStep::DaemonStep(DaemonStepNode { daemon_ref: l.1, loc: l.0 })),
3140            // §λ-L-E Fase 13 — Mobile typed channels (paper §3.1, §3.2, §4.3)
3141            TokenType::Emit => self.parse_emit_step(),
3142            // §Fase 92.b — `mint <Credential> as <binding>` (ephemeral credential).
3143            TokenType::Mint => self.parse_mint_step(),
3144            // §Fase 94.b — `rotate <SecretsStore> [where "…"] with <Tool> as
3145            // <binding>` (mediated secret renewal).
3146            TokenType::Rotate => self.parse_rotate_step(),
3147            TokenType::Publish => self.parse_publish_step(),
3148            TokenType::Discover => self.parse_discover_step(),
3149            TokenType::Persist => self.parse_persist_step(),
3150            TokenType::Retrieve => self.parse_retrieve_step(),
3151            TokenType::Mutate => self.parse_mutate_step(),
3152            TokenType::Purge => self.parse_store_where_step().map(|(loc, store_name, where_expr)| FlowStep::Purge(PurgeStep { store_name, where_expr, loc })),
3153            TokenType::Transact => self.parse_block_step("transact").map(|l| FlowStep::Transact(TransactBlock { loc: l })),
3154            // §Fase 88.a — the `warden` adversarial-analysis block.
3155            TokenType::Warden => self.parse_warden().map(FlowStep::Warden),
3156            // §Fase 51.a — the `quant` cognitive block (Hilbert-space projection).
3157            TokenType::Quant => self.parse_quant().map(FlowStep::Quant),
3158            // §Fase 51.d.2 — the `yield` measurement point.
3159            TokenType::Yield => self.parse_yield().map(FlowStep::Yield),
3160            // §Fase 52.c — `run <Flow>(args)` as a flow-step: invoke a declared
3161            // flow from inside a body (a `daemon` listen handler, Q3). Reuses
3162            // the top-level run parser.
3163            TokenType::Run => self.parse_run().map(FlowStep::Run),
3164
3165            _ => {
3166                // §Fase 28.e — append "Did you mean X?" hint when the
3167                // unknown token looks like a typo'd flow-body keyword
3168                // (e.g. `stepp` / `reasn` / `validte`). D3, D11.
3169                let hint = crate::smart_suggest::suggest_for(
3170                    &tok.value,
3171                    crate::smart_suggest::FLOW_BODY_KEYWORD_NAMES,
3172                );
3173                let base = format!(
3174                    "Unexpected token in flow body: '{}' — expected step, if, for, let, return, ...",
3175                    tok.value
3176                );
3177                let message = if hint.is_empty() {
3178                    base
3179                } else {
3180                    format!("{base}. {hint}")
3181                };
3182                Err(ParseError {
3183                    message,
3184                    line: tok.line,
3185                    column: tok.column,
3186                    ..Default::default()
3187                })
3188            }
3189        }
3190    }
3191
3192    // ── STEP ─────────────────────────────────────────────────────
3193
3194    fn parse_step(&mut self) -> Result<StepNode, ParseError> {
3195        let tok = self.consume(TokenType::Step)?;
3196        let loc = self.loc_of(&tok);
3197        let name = self.consume(TokenType::Identifier)?.value;
3198
3199        let mut persona_ref = String::new();
3200        if self.check(TokenType::Use) {
3201            self.advance();
3202            persona_ref = self.consume_any_ident_or_kw()?.value;
3203        }
3204
3205        self.consume(TokenType::LBrace)?;
3206
3207        let mut node = StepNode {
3208            name,
3209            persona_ref,
3210            given: String::new(),
3211            ask: String::new(),
3212            output_type: String::new(),
3213            confidence_floor: None,
3214            navigate_ref: String::new(),
3215            apply_ref: String::new(),
3216            requires_context: None,
3217            now_tz: None,
3218            loc,
3219        };
3220
3221        while !self.check(TokenType::RBrace) {
3222            let inner = self.current().clone();
3223
3224            match inner.ttype {
3225                TokenType::Given => {
3226                    self.advance();
3227                    self.consume(TokenType::Colon)?;
3228                    node.given = self.parse_expression_string()?;
3229                }
3230                TokenType::Ask => {
3231                    self.advance();
3232                    self.consume(TokenType::Colon)?;
3233                    node.ask = self.consume(TokenType::StringLit)?.value;
3234                }
3235                TokenType::Output => {
3236                    // Mirror of Python `_parse_step` `case "output":`
3237                    // which uses `_parse_output_type_string` — accepts
3238                    // the FULL generic-aware shape `Stream<T>`,
3239                    // `Stream<T>?`, `Identifier?`, NOT just the bare
3240                    // head identifier. Pre-fix the step parser dropped
3241                    // `<T>` and downstream `flow_has_stream_output`'s
3242                    // `starts_with("Stream<") && ends_with('>')` then
3243                    // returned false → `implicit_transport == "json"`
3244                    // → dynamic routes served JSON instead of SSE.
3245                    self.advance();
3246                    self.consume(TokenType::Colon)?;
3247                    node.output_type = self.parse_output_type_string()?;
3248                }
3249                TokenType::Navigate => {
3250                    self.advance();
3251                    self.consume(TokenType::Colon)?;
3252                    node.navigate_ref = self.parse_dotted_identifier()?;
3253                }
3254                TokenType::Identifier if inner.value == "confidence_floor" => {
3255                    self.advance();
3256                    self.consume(TokenType::Colon)?;
3257                    node.confidence_floor = Some(self.consume_number()?);
3258                }
3259                TokenType::Identifier if inner.value == "apply" => {
3260                    self.advance();
3261                    self.consume(TokenType::Colon)?;
3262                    node.apply_ref = self.consume_any_ident_or_kw()?.value;
3263                }
3264                // §Fase 68.b — `requires_context: <tokens>`: the step's declared
3265                // model-capability requirement (the context window the cognition
3266                // needs). A bare positive integer literal; the §68.c resolver maps
3267                // it to a concrete model. Range/ceiling is the type-checker's job
3268                // (§68.b positive-int + §68.f catalog ceiling) — the parser only
3269                // requires an integer token here (a float / non-number is a parse
3270                // error, surfaced at the exact column).
3271                TokenType::Identifier if inner.value == "requires_context" => {
3272                    self.advance();
3273                    self.consume(TokenType::Colon)?;
3274                    let num = self.current().clone();
3275                    let bad = |tok: &crate::tokens::Token| ParseError {
3276                        message: format!(
3277                            "`requires_context:` must be a positive integer token count \
3278                             (got '{}')",
3279                            tok.value
3280                        ),
3281                        line: tok.line,
3282                        column: tok.column,
3283                        ..Default::default()
3284                    };
3285                    if num.ttype != TokenType::Integer {
3286                        return Err(bad(&num));
3287                    }
3288                    let value = num.value.parse::<u32>().map_err(|_| bad(&num))?;
3289                    self.advance();
3290                    node.requires_context = Some(value);
3291                }
3292                // §Fase 91.a — `now: "<IANA-tz>"`: the step's declared cognitive
3293                // timezone. A string literal; the format law (IANA shape) is the
3294                // type-checker's job (`axon-T892`) — the parser only requires a
3295                // string token here, surfaced at the exact column.
3296                TokenType::Identifier if inner.value == "now" => {
3297                    self.advance();
3298                    self.consume(TokenType::Colon)?;
3299                    let tz = self.current().clone();
3300                    if tz.ttype != TokenType::StringLit {
3301                        return Err(ParseError {
3302                            message: format!(
3303                                "`now:` must be an IANA timezone string literal like \
3304                                 \"America/Bogota\" or \"UTC\" (got '{}')",
3305                                tz.value
3306                            ),
3307                            line: tz.line,
3308                            column: tz.column,
3309                            ..Default::default()
3310                        });
3311                    }
3312                    self.advance();
3313                    node.now_tz = Some(tz.value);
3314                }
3315                // §Fase 54.a — a `use` nested inside a `step { }` body used
3316                // to be skipped structurally (grouped with the sub-constructs
3317                // below), silently degrading the tool dispatch to an
3318                // unconstrained LLM step with NO diagnostic. That fallthrough
3319                // drops the AST node before the type-checker can see it, so the
3320                // resource the tool would provision is never linearly accounted
3321                // for (use_tool soundness). Reject it here, at the parser —
3322                // the only place that still sees the token — and redirect to
3323                // the canonical forms.
3324                TokenType::Use => {
3325                    let tool = self
3326                        .tokens
3327                        .get(self.pos + 1)
3328                        .map(|t| t.value.as_str())
3329                        .filter(|v| !v.is_empty())
3330                        .unwrap_or("<Tool>");
3331                    return Err(ParseError {
3332                        message: format!(
3333                            "`use` is not valid inside a `step {{ }}` body — the tool dispatch \
3334                             would be silently dropped. To invoke a tool, either write the \
3335                             flow-level step `use {tool} on <arg>` (outside this block), or bind \
3336                             it inside this step with `apply: {tool}`. To attach a persona, put \
3337                             it in the step header: `step <name> use <Persona> {{ … }}`."
3338                        ),
3339                        line: inner.line,
3340                        column: inner.column,
3341                        ..Default::default()
3342                    });
3343                }
3344                // Sub-constructs (probe, reason, weave, stream) → skip structurally
3345                TokenType::Probe
3346                | TokenType::Reason
3347                | TokenType::Weave
3348                | TokenType::Stream => {
3349                    self.skip_flow_step_structural()?;
3350                }
3351                _ => {
3352                    return Err(ParseError {
3353                        message: format!(
3354                            "Unexpected token in step body: '{}' — expected given, ask, \
3355                             probe, reason, weave, stream, output, confidence_floor, navigate, \
3356                             apply, requires_context, now",
3357                            inner.value
3358                        ),
3359                        line: inner.line,
3360                        column: inner.column,
3361                                            ..Default::default()
3362                    });
3363                }
3364            }
3365        }
3366        self.consume(TokenType::RBrace)?;
3367        Ok(node)
3368    }
3369
3370    /// Skip a flow-level sub-construct structurally (consume keyword + args + optional block).
3371    fn skip_flow_step_structural(&mut self) -> Result<(), ParseError> {
3372        // Consume the keyword
3373        self.advance();
3374        // Consume tokens until we hit a { or a closing }, or a known flow step keyword
3375        while !self.check(TokenType::LBrace)
3376            && !self.check(TokenType::RBrace)
3377            && !self.check(TokenType::Eof)
3378        {
3379            // Check if we hit a new step-level keyword (means this was a one-liner)
3380            let tt = &self.current().ttype;
3381            if matches!(
3382                tt,
3383                TokenType::Step
3384                    | TokenType::Given
3385                    | TokenType::Ask
3386                    | TokenType::Output
3387                    | TokenType::Navigate
3388                    | TokenType::Use
3389                    | TokenType::Probe
3390                    | TokenType::Reason
3391                    | TokenType::Weave
3392                    | TokenType::Stream
3393                    | TokenType::If
3394                    | TokenType::For
3395                    | TokenType::Let
3396                    | TokenType::Return
3397            ) {
3398                return Ok(());
3399            }
3400            self.advance();
3401        }
3402        // If block, skip it
3403        if self.check(TokenType::LBrace) {
3404            self.skip_braced_block()?;
3405        }
3406        Ok(())
3407    }
3408
3409    // ── INTENT ───────────────────────────────────────────────────
3410
3411    fn parse_intent(&mut self) -> Result<IntentNode, ParseError> {
3412        let tok = self.consume(TokenType::Intent)?;
3413        let loc = self.loc_of(&tok);
3414        let name = self.consume(TokenType::Identifier)?.value;
3415        self.consume(TokenType::LBrace)?;
3416
3417        let mut node = IntentNode {
3418            name,
3419            given: String::new(),
3420            ask: String::new(),
3421            output_type: None,
3422            confidence_floor: None,
3423            loc,
3424            leading_trivia: Vec::new(),
3425            trailing_trivia: Vec::new(),
3426        };
3427
3428        while !self.check(TokenType::RBrace) {
3429            let field_name = self.current().value.clone();
3430            self.advance();
3431            self.consume(TokenType::Colon)?;
3432
3433            match field_name.as_str() {
3434                "given" => node.given = self.consume(TokenType::Identifier)?.value,
3435                "ask" => node.ask = self.consume(TokenType::StringLit)?.value,
3436                "output" => node.output_type = Some(self.parse_type_expr()?),
3437                "confidence_floor" => node.confidence_floor = Some(self.consume_number()?),
3438                _ => self.skip_value(),
3439            }
3440        }
3441        self.consume(TokenType::RBrace)?;
3442        Ok(node)
3443    }
3444
3445    // ── RUN ──────────────────────────────────────────────────────
3446
3447    fn parse_run(&mut self) -> Result<RunStatement, ParseError> {
3448        let tok = self.consume(TokenType::Run)?;
3449        let loc = self.loc_of(&tok);
3450        let flow_name = self.consume(TokenType::Identifier)?.value;
3451
3452        self.consume(TokenType::LParen)?;
3453        let mut arguments = Vec::new();
3454        if !self.check(TokenType::RParen) {
3455            arguments = self.parse_argument_list()?;
3456        }
3457        self.consume(TokenType::RParen)?;
3458
3459        let mut node = RunStatement {
3460            flow_name,
3461            arguments,
3462            persona: String::new(),
3463            context: String::new(),
3464            anchors: Vec::new(),
3465            on_failure: String::new(),
3466            on_failure_params: Vec::new(),
3467            output_to: String::new(),
3468            effort: String::new(),
3469            loc,
3470            leading_trivia: Vec::new(),
3471            trailing_trivia: Vec::new(),
3472        };
3473
3474        while self.check_run_modifier() {
3475            let mod_tok = self.current().clone();
3476            match mod_tok.ttype {
3477                TokenType::As => {
3478                    self.advance();
3479                    node.persona = self.consume(TokenType::Identifier)?.value;
3480                }
3481                TokenType::Within => {
3482                    self.advance();
3483                    node.context = self.consume(TokenType::Identifier)?.value;
3484                }
3485                TokenType::ConstrainedBy => {
3486                    self.advance();
3487                    node.anchors = self.parse_bracketed_identifiers()?;
3488                }
3489                TokenType::OnFailure => {
3490                    self.advance();
3491                    self.consume(TokenType::Colon)?;
3492                    node.on_failure = self.consume_any_ident_or_kw()?.value;
3493                    // Parse optional params: (key: val, ...)
3494                    if self.check(TokenType::LParen) {
3495                        self.advance();
3496                        while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
3497                            let key = self.consume_any_ident_or_kw()?.value;
3498                            self.consume(TokenType::Colon)?;
3499                            let val = self.consume_any_ident_or_kw()?.value;
3500                            node.on_failure_params.push((key, val));
3501                            if self.check(TokenType::Comma) {
3502                                self.advance();
3503                            }
3504                        }
3505                        if self.check(TokenType::RParen) {
3506                            self.advance();
3507                        }
3508                    }
3509                }
3510                TokenType::OutputTo => {
3511                    self.advance();
3512                    self.consume(TokenType::Colon)?;
3513                    node.output_to = self.consume(TokenType::StringLit)?.value;
3514                }
3515                TokenType::Effort => {
3516                    self.advance();
3517                    self.consume(TokenType::Colon)?;
3518                    node.effort = self.consume_any_ident_or_kw()?.value;
3519                }
3520                _ => break,
3521            }
3522        }
3523
3524        Ok(node)
3525    }
3526
3527    // ── EPISTEMIC BLOCK ──────────────────────────────────────────
3528
3529    fn parse_epistemic_block(&mut self) -> Result<EpistemicBlock, ParseError> {
3530        let tok = self.current().clone();
3531        let mode = match tok.ttype {
3532            TokenType::Know => "know",
3533            TokenType::Believe => "believe",
3534            TokenType::Speculate => "speculate",
3535            TokenType::Doubt => "doubt",
3536            _ => unreachable!(),
3537        };
3538        self.advance();
3539        let loc = self.loc_of(&tok);
3540
3541        self.consume(TokenType::LBrace)?;
3542        let mut body = Vec::new();
3543        while !self.check(TokenType::RBrace) {
3544            body.push(self.parse_declaration()?);
3545        }
3546        self.consume(TokenType::RBrace)?;
3547
3548        Ok(EpistemicBlock {
3549            mode: mode.to_string(),
3550            body,
3551            loc,
3552            leading_trivia: Vec::new(),
3553            trailing_trivia: Vec::new(),
3554        })
3555    }
3556
3557    // ── IF ────────────────────────────────────────────────────────
3558
3559    // ── §Fase 70.a — the pure expression engine (Pratt parser) ───────────
3560
3561    /// Parse a pure expression (§Fase 70). Precedence-climbing: `or` < `and` <
3562    /// comparison < `+ -` < `* / %` < unary (`- not`) < atom. Total + pure; no
3563    /// side effects. Field/index access + the builtin catalog land in §70.c/d.
3564    fn parse_expr(&mut self) -> Result<Expr, ParseError> {
3565        self.parse_expr_bp(0)
3566    }
3567
3568    fn parse_expr_bp(&mut self, min_bp: u8) -> Result<Expr, ParseError> {
3569        // Prefix: unary `-` (negation) / `not` (boolean). Binds tighter than
3570        // every binary operator (bp 6).
3571        let mut lhs = match self.current().ttype {
3572            TokenType::Minus => {
3573                self.advance();
3574                Expr::Unary(UnOp::Neg, Box::new(self.parse_expr_bp(6)?))
3575            }
3576            TokenType::Not => {
3577                self.advance();
3578                Expr::Unary(UnOp::Not, Box::new(self.parse_expr_bp(6)?))
3579            }
3580            _ => self.parse_postfix()?,
3581        };
3582        // Infix: left-associative (right_bp = left_bp + 1).
3583        while let Some((op, lbp)) = Self::binop_of(self.current().ttype.clone()) {
3584            if lbp < min_bp {
3585                break;
3586            }
3587            self.advance();
3588            let rhs = self.parse_expr_bp(lbp + 1)?;
3589            lhs = Expr::Binary(op, Box::new(lhs), Box::new(rhs));
3590        }
3591        Ok(lhs)
3592    }
3593
3594    /// Map a token to `(BinOp, left binding power)`, or `None` if it is not an
3595    /// infix operator (which stops the climb — e.g. at `->` or `{`).
3596    fn binop_of(t: TokenType) -> Option<(BinOp, u8)> {
3597        Some(match t {
3598            TokenType::Or => (BinOp::Or, 1),
3599            TokenType::And => (BinOp::And, 2),
3600            TokenType::Eq => (BinOp::Eq, 3),
3601            TokenType::Neq => (BinOp::Ne, 3),
3602            TokenType::Lt => (BinOp::Lt, 3),
3603            TokenType::Lte => (BinOp::Le, 3),
3604            TokenType::Gt => (BinOp::Gt, 3),
3605            TokenType::Gte => (BinOp::Ge, 3),
3606            TokenType::Plus => (BinOp::Add, 4),
3607            TokenType::Minus => (BinOp::Sub, 4),
3608            TokenType::Star => (BinOp::Mul, 5),
3609            TokenType::Slash => (BinOp::Div, 5),
3610            TokenType::Percent => (BinOp::Mod, 5),
3611            _ => return None,
3612        })
3613    }
3614
3615    /// §Fase 70.c — parse a primary then its `.` postfix chain: a builtin call
3616    /// (`.length`, `.contains(x)`) when the name is in the closed catalog, else
3617    /// a dotted reference-path continuation (`a.b.c` → `Ref("a.b.c")`, the
3618    /// pre-§70.c behaviour). Field access on a non-reference (`(a+b).x`) is
3619    /// reserved for §70.d.
3620    fn parse_postfix(&mut self) -> Result<Expr, ParseError> {
3621        let mut expr = self.parse_expr_atom()?;
3622        loop {
3623            if self.check(TokenType::Dot) {
3624                self.advance();
3625                let name = self.consume_any_ident_or_kw()?.value;
3626                if let Some(builtin) = Builtin::from_name(&name) {
3627                    let mut args = vec![expr];
3628                    if self.check(TokenType::LParen) {
3629                        self.advance();
3630                        while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
3631                            args.push(self.parse_expr_bp(0)?);
3632                            if self.check(TokenType::Comma) {
3633                                self.advance();
3634                            } else {
3635                                break;
3636                            }
3637                        }
3638                        self.consume(TokenType::RParen)?;
3639                    }
3640                    expr = Expr::Call(builtin, args);
3641                } else {
3642                    // §Fase 70.d — a plain dotted path on a Ref extends the Ref
3643                    // (back-compat: `a.b.c` → `Ref("a.b.c")`); on any other base
3644                    // it is a structured field access (the JSONB seam).
3645                    expr = match expr {
3646                        Expr::Ref(p) => Expr::Ref(format!("{p}.{name}")),
3647                        other => Expr::Field(Box::new(other), name),
3648                    };
3649                }
3650            } else if self.check(TokenType::LBracket) {
3651                // §Fase 70.d — index access `base[index]`.
3652                self.advance();
3653                let index = self.parse_expr_bp(0)?;
3654                self.consume(TokenType::RBracket)?;
3655                expr = Expr::Index(Box::new(expr), Box::new(index));
3656            } else {
3657                break;
3658            }
3659        }
3660        Ok(expr)
3661    }
3662
3663    fn parse_expr_atom(&mut self) -> Result<Expr, ParseError> {
3664        let tok = self.current().clone();
3665        match tok.ttype {
3666            TokenType::Integer => {
3667                self.advance();
3668                let lit = tok
3669                    .value
3670                    .parse::<i64>()
3671                    .map(ExprLit::Int)
3672                    .or_else(|_| tok.value.parse::<f64>().map(ExprLit::Float))
3673                    .map_err(|_| ParseError {
3674                        message: format!("invalid integer literal '{}'", tok.value),
3675                        line: tok.line,
3676                        column: tok.column,
3677                        ..Default::default()
3678                    })?;
3679                Ok(Expr::Lit(lit))
3680            }
3681            TokenType::Float => {
3682                self.advance();
3683                let f = tok.value.parse::<f64>().map_err(|_| ParseError {
3684                    message: format!("invalid float literal '{}'", tok.value),
3685                    line: tok.line,
3686                    column: tok.column,
3687                    ..Default::default()
3688                })?;
3689                Ok(Expr::Lit(ExprLit::Float(f)))
3690            }
3691            TokenType::Bool => {
3692                self.advance();
3693                Ok(Expr::Lit(ExprLit::Bool(tok.value == "true")))
3694            }
3695            TokenType::StringLit => {
3696                self.advance();
3697                Ok(Expr::Lit(ExprLit::Str(tok.value)))
3698            }
3699            TokenType::LParen => {
3700                self.advance();
3701                let inner = self.parse_expr_bp(0)?;
3702                self.consume(TokenType::RParen)?;
3703                Ok(inner)
3704            }
3705            _ => {
3706                // Reference: a single identifier (or keyword used as a name).
3707                // The `.` chain (dotted path / builtin call) is handled by the
3708                // postfix layer (§70.c `parse_postfix`).
3709                Ok(Expr::Ref(self.consume_any_ident_or_kw()?.value))
3710            }
3711        }
3712    }
3713
3714    /// §Fase 70.a — render a literal to its legacy surface string (for the
3715    /// back-compat `(condition, op, value)` triple). Only used when an
3716    /// expression fits the legacy shape; numeric round-tripping is exact for
3717    /// ints and faithful-enough for floats (the legacy runtime re-parses it).
3718    fn expr_lit_surface(lit: &ExprLit) -> String {
3719        match lit {
3720            ExprLit::Int(i) => i.to_string(),
3721            ExprLit::Float(f) => f.to_string(),
3722            ExprLit::Bool(b) => b.to_string(),
3723            ExprLit::Str(s) => s.clone(),
3724        }
3725    }
3726
3727    fn expr_leaf_surface(expr: &Expr) -> Option<String> {
3728        match expr {
3729            Expr::Ref(p) => Some(p.clone()),
3730            Expr::Lit(l) => Some(Self::expr_lit_surface(l)),
3731            _ => None,
3732        }
3733    }
3734
3735    /// A legacy "leaf" is a bare reference (truthy check) or a
3736    /// `<ref> <cmp> <ref|literal>` triple — exactly what the pre-§70 `if`
3737    /// grammar could express.
3738    fn expr_legacy_leaf(expr: &Expr) -> Option<(String, String, String)> {
3739        match expr {
3740            Expr::Ref(p) => Some((p.clone(), String::new(), String::new())),
3741            Expr::Binary(op, l, r) => {
3742                let op_s = match op {
3743                    BinOp::Eq => "==",
3744                    BinOp::Ne => "!=",
3745                    BinOp::Lt => "<",
3746                    BinOp::Le => "<=",
3747                    BinOp::Gt => ">",
3748                    BinOp::Ge => ">=",
3749                    _ => return None,
3750                };
3751                let lhs = match &**l {
3752                    Expr::Ref(p) => p.clone(),
3753                    _ => return None,
3754                };
3755                let rhs = Self::expr_leaf_surface(r)?;
3756                Some((lhs, op_s.to_string(), rhs))
3757            }
3758            _ => None,
3759        }
3760    }
3761
3762    /// Flatten an `or`-tree of legacy leaves in left-to-right order. Returns
3763    /// `false` (and leaves `out` unusable) if any node is not a legacy leaf.
3764    fn collect_or_leaves(expr: &Expr, out: &mut Vec<(String, String, String)>) -> bool {
3765        match expr {
3766            Expr::Binary(BinOp::Or, l, r) => {
3767                Self::collect_or_leaves(l, out) && Self::collect_or_leaves(r, out)
3768            }
3769            _ => match Self::expr_legacy_leaf(expr) {
3770                Some(t) => {
3771                    out.push(t);
3772                    true
3773                }
3774                None => false,
3775            },
3776        }
3777    }
3778
3779    /// §Fase 70.a — if the parsed condition fits the legacy
3780    /// `(condition, op, value)` + `or`-chain shape, return the legacy fields so
3781    /// the IR + runtime stay byte-identical to pre-§70 (zero drift). `None` ⇒
3782    /// the condition uses richer forms (`and`, `not`, arithmetic, parentheses,
3783    /// nesting) and must ride the `cond` expression evaluator.
3784    #[allow(clippy::type_complexity)]
3785    fn cond_as_legacy(
3786        expr: &Expr,
3787    ) -> Option<(String, String, String, Vec<(String, String, String)>, String)> {
3788        let mut leaves = Vec::new();
3789        if !Self::collect_or_leaves(expr, &mut leaves) || leaves.is_empty() {
3790            return None;
3791        }
3792        let (c0, o0, v0) = leaves[0].clone();
3793        let rest = leaves[1..].to_vec();
3794        let conjunctor = if rest.is_empty() {
3795            String::new()
3796        } else {
3797            "or".to_string()
3798        };
3799        Some((c0, o0, v0, rest, conjunctor))
3800    }
3801
3802    fn parse_if(&mut self) -> Result<ConditionalNode, ParseError> {
3803        let tok = self.consume(TokenType::If)?;
3804        let loc = self.loc_of(&tok);
3805
3806        // §Fase 70.a — parse the condition as a pure expression, then split:
3807        // a legacy-expressible condition populates the legacy triple fields
3808        // (cond = None → byte-identical IR + eval); a richer condition rides
3809        // the `cond` expression evaluator.
3810        let expr = self.parse_expr()?;
3811        let (condition, comparison_op, comparison_value, conditions, conjunctor, cond) =
3812            match Self::cond_as_legacy(&expr) {
3813                Some((c, o, v, more, conj)) => (c, o, v, more, conj, None),
3814                None => (
3815                    String::new(),
3816                    String::new(),
3817                    String::new(),
3818                    Vec::new(),
3819                    String::new(),
3820                    Some(expr),
3821                ),
3822            };
3823
3824        let mut then_body = Vec::new();
3825        let mut else_body = Vec::new();
3826
3827        // Arrow form or block form
3828        if self.check(TokenType::Arrow) {
3829            self.advance();
3830            then_body.push(self.parse_flow_step()?);
3831        } else if self.check(TokenType::LBrace) {
3832            self.advance();
3833            while !self.check(TokenType::RBrace) {
3834                then_body.push(self.parse_flow_step()?);
3835            }
3836            self.consume(TokenType::RBrace)?;
3837        }
3838
3839        // Else branch
3840        if self.check(TokenType::Else) {
3841            self.advance();
3842            if self.check(TokenType::Arrow) {
3843                self.advance();
3844                else_body.push(self.parse_flow_step()?);
3845            } else if self.check(TokenType::LBrace) {
3846                self.advance();
3847                while !self.check(TokenType::RBrace) {
3848                    else_body.push(self.parse_flow_step()?);
3849                }
3850                self.consume(TokenType::RBrace)?;
3851            }
3852        }
3853
3854        Ok(ConditionalNode {
3855            condition,
3856            comparison_op,
3857            comparison_value,
3858            then_body,
3859            else_body,
3860            conditions,
3861            conjunctor,
3862            cond,
3863            loc,
3864        })
3865    }
3866
3867    // ── FOR IN ───────────────────────────────────────────────────
3868
3869    fn parse_for_in(&mut self) -> Result<ForInStatement, ParseError> {
3870        let tok = self.consume(TokenType::For)?;
3871        let loc = self.loc_of(&tok);
3872        let variable = self.consume(TokenType::Identifier)?.value;
3873        self.consume(TokenType::In)?;
3874        let iterable = self.parse_dotted_identifier()?;
3875
3876        self.consume(TokenType::LBrace)?;
3877        // Fase 19.e — increment loop_depth so `parse_break` /
3878        // `parse_continue` inside the body pass the scope check.
3879        // Decrement on every exit path (Ok / Err) so a parse error
3880        // mid-body does not leave the depth permanently elevated
3881        // for later top-level parsing — `?` would skip the
3882        // decrement otherwise.
3883        self.loop_depth += 1;
3884        let body_result = (|| -> Result<Vec<FlowStep>, ParseError> {
3885            let mut body = Vec::new();
3886            while !self.check(TokenType::RBrace) {
3887                body.push(self.parse_flow_step()?);
3888            }
3889            Ok(body)
3890        })();
3891        self.loop_depth -= 1;
3892        let body = body_result?;
3893        self.consume(TokenType::RBrace)?;
3894
3895        Ok(ForInStatement {
3896            variable,
3897            iterable,
3898            body,
3899            loc,
3900        })
3901    }
3902
3903    /// Fase 19.e — `break` keyword. Compile-time scope check
3904    /// (`loop_depth == 0`) rejects break outside a for-in body.
3905    fn parse_break(&mut self) -> Result<BreakStatement, ParseError> {
3906        let tok = self.consume(TokenType::Break)?;
3907        let loc = self.loc_of(&tok);
3908        if self.loop_depth == 0 {
3909            return Err(ParseError {
3910                message: "'break' outside of a for-in loop body".to_string(),
3911                line: tok.line,
3912                column: tok.column,
3913                            ..Default::default()
3914            });
3915        }
3916        Ok(BreakStatement { loc })
3917    }
3918
3919    /// Fase 19.e — `continue` keyword. Same scope check as
3920    /// `parse_break`.
3921    fn parse_continue(&mut self) -> Result<ContinueStatement, ParseError> {
3922        let tok = self.consume(TokenType::Continue)?;
3923        let loc = self.loc_of(&tok);
3924        if self.loop_depth == 0 {
3925            return Err(ParseError {
3926                message: "'continue' outside of a for-in loop body".to_string(),
3927                line: tok.line,
3928                column: tok.column,
3929                            ..Default::default()
3930            });
3931        }
3932        Ok(ContinueStatement { loc })
3933    }
3934
3935    // ── LET ──────────────────────────────────────────────────────
3936
3937    fn parse_let(&mut self) -> Result<LetStatement, ParseError> {
3938        let tok = self.consume(TokenType::Let)?;
3939        let loc = self.loc_of(&tok);
3940
3941        // Name can be an identifier or a keyword used as binding name
3942        let name = self.consume_any_ident_or_kw()?.value;
3943        // §Fase 51.c.3 — optional type annotation `let x: <TypeExpr> = …`.
3944        let type_annotation = if self.check(TokenType::Colon) {
3945            self.advance();
3946            Some(self.parse_type_expr()?)
3947        } else {
3948            None
3949        };
3950        self.consume(TokenType::Assign)?;
3951        // Fase 17.a — reset side-channel before parsing value; the
3952        // atom / expr helpers tag the kind as they descend.
3953        self.last_let_value_kind = "literal".to_string();
3954        let (value, value_ast) = self.parse_let_value_expr_with_ast()?;
3955
3956        Ok(LetStatement {
3957            identifier: name,
3958            value_expr: value,
3959            value_kind: self.last_let_value_kind.clone(),
3960            type_annotation,
3961            value_ast,
3962            loc,
3963            leading_trivia: Vec::new(),
3964            trailing_trivia: Vec::new(),
3965        })
3966    }
3967
3968    fn parse_let_value_expr(&mut self) -> Result<String, ParseError> {
3969        let atom = self.parse_let_atom()?;
3970
3971        // Arithmetic expression: collect as string
3972        if matches!(
3973            self.current().ttype,
3974            TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
3975        ) {
3976            let mut parts = vec![atom];
3977            while matches!(
3978                self.current().ttype,
3979                TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
3980            ) {
3981                parts.push(self.advance().value.clone());
3982                parts.push(self.parse_let_atom()?);
3983            }
3984            self.last_let_value_kind = "expression".to_string();
3985            return Ok(parts.join(" "));
3986        }
3987        Ok(atom)
3988    }
3989
3990    /// §Fase 70.f — parse a `let`-binding value, additionally producing a
3991    /// structured `value_ast` for the expression case. A list literal keeps the
3992    /// dedicated path; everything else is parsed through the §70 expression
3993    /// engine and classified: a bare literal / reference keeps its pre-§70
3994    /// string form (`value_ast = None`, byte-identical), while a real expression
3995    /// (`price * qty`, `recent.length`) additionally carries a `value_ast` the
3996    /// runtime evaluates for real (pre-§70.f it was treated as an opaque literal
3997    /// string). Used ONLY by `parse_let` — other value positions (list items,
3998    /// remember/stream values) keep the string-only `parse_let_value_expr`.
3999    fn parse_let_value_expr_with_ast(&mut self) -> Result<(String, Option<Expr>), ParseError> {
4000        if self.check(TokenType::LBracket) {
4001            self.last_let_value_kind = "literal".to_string();
4002            return Ok((self.parse_let_list_literal()?, None));
4003        }
4004        let expr = self.parse_expr()?;
4005        Ok(match expr {
4006            Expr::Lit(lit) => {
4007                self.last_let_value_kind = "literal".to_string();
4008                (Self::expr_lit_surface(&lit), None)
4009            }
4010            Expr::Ref(p) => {
4011                self.last_let_value_kind = "reference".to_string();
4012                (p, None)
4013            }
4014            other => {
4015                self.last_let_value_kind = "expression".to_string();
4016                (Self::render_expr(&other), Some(other))
4017            }
4018        })
4019    }
4020
4021    /// §Fase 70.f — a readable surface rendering of an expression for the
4022    /// vestigial `value_expr` string (the runtime uses `value_ast`).
4023    fn render_expr(e: &Expr) -> String {
4024        match e {
4025            Expr::Lit(l) => Self::expr_lit_surface(l),
4026            Expr::Ref(p) => p.clone(),
4027            Expr::Unary(UnOp::Neg, x) => format!("-{}", Self::render_expr(x)),
4028            Expr::Unary(UnOp::Not, x) => format!("not {}", Self::render_expr(x)),
4029            Expr::Binary(op, l, r) => {
4030                let sym = match op {
4031                    BinOp::Add => "+",
4032                    BinOp::Sub => "-",
4033                    BinOp::Mul => "*",
4034                    BinOp::Div => "/",
4035                    BinOp::Mod => "%",
4036                    BinOp::Eq => "==",
4037                    BinOp::Ne => "!=",
4038                    BinOp::Lt => "<",
4039                    BinOp::Le => "<=",
4040                    BinOp::Gt => ">",
4041                    BinOp::Ge => ">=",
4042                    BinOp::And => "and",
4043                    BinOp::Or => "or",
4044                };
4045                format!("({} {sym} {})", Self::render_expr(l), Self::render_expr(r))
4046            }
4047            Expr::Call(b, args) => {
4048                let recv = args.first().map(Self::render_expr).unwrap_or_default();
4049                let rest: Vec<String> = args.iter().skip(1).map(Self::render_expr).collect();
4050                if rest.is_empty() {
4051                    format!("{recv}.{}", b.surface())
4052                } else {
4053                    format!("{recv}.{}({})", b.surface(), rest.join(", "))
4054                }
4055            }
4056            Expr::Field(b, f) => format!("{}.{f}", Self::render_expr(b)),
4057            Expr::Index(b, i) => format!("{}[{}]", Self::render_expr(b), Self::render_expr(i)),
4058        }
4059    }
4060
4061    fn parse_let_atom(&mut self) -> Result<String, ParseError> {
4062        let tok = self.current().clone();
4063
4064        match tok.ttype {
4065            TokenType::StringLit => {
4066                self.last_let_value_kind = "literal".to_string();
4067                self.advance();
4068                Ok(tok.value)
4069            }
4070            TokenType::Integer | TokenType::Float => {
4071                self.last_let_value_kind = "literal".to_string();
4072                self.advance();
4073                Ok(tok.value)
4074            }
4075            TokenType::Bool => {
4076                self.last_let_value_kind = "literal".to_string();
4077                self.advance();
4078                Ok(tok.value)
4079            }
4080            TokenType::Identifier => {
4081                self.last_let_value_kind = "reference".to_string();
4082                self.parse_dotted_identifier()
4083            }
4084            TokenType::LBracket => {
4085                self.last_let_value_kind = "literal".to_string();
4086                self.parse_let_list_literal()
4087            }
4088            _ => {
4089                // Keywords starting a dotted path (pix.document_tree)
4090                if self.pos + 1 < self.tokens.len()
4091                    && self.tokens[self.pos + 1].ttype == TokenType::Dot
4092                {
4093                    self.last_let_value_kind = "reference".to_string();
4094                    return self.parse_dotted_identifier();
4095                }
4096                Err(ParseError {
4097                    message: format!(
4098                        "Expected value expression, found {:?}('{}')",
4099                        tok.ttype, tok.value
4100                    ),
4101                    line: tok.line,
4102                    column: tok.column,
4103                                    ..Default::default()
4104                })
4105            }
4106        }
4107    }
4108
4109    fn parse_let_list_literal(&mut self) -> Result<String, ParseError> {
4110        self.consume(TokenType::LBracket)?;
4111        let mut items = Vec::new();
4112        if !self.check(TokenType::RBracket) {
4113            items.push(self.parse_let_value_expr()?);
4114            while self.check(TokenType::Comma) {
4115                self.advance();
4116                if self.check(TokenType::RBracket) {
4117                    break; // trailing comma
4118                }
4119                items.push(self.parse_let_value_expr()?);
4120            }
4121        }
4122        self.consume(TokenType::RBracket)?;
4123        Ok(format!("[{}]", items.join(", ")))
4124    }
4125
4126    // ── RETURN ───────────────────────────────────────────────────
4127
4128    fn parse_return(&mut self) -> Result<ReturnStatement, ParseError> {
4129        let tok = self.consume(TokenType::Return)?;
4130        let loc = self.loc_of(&tok);
4131        let value = self.parse_let_value_expr()?;
4132        Ok(ReturnStatement {
4133            value_expr: value,
4134            loc,
4135        })
4136    }
4137
4138    // ── TIER 2 FLOW STEP HELPERS ────────────────────────────────────
4139
4140    /// Parse: keyword target (consumes keyword + one identifier/keyword-as-value).
4141    fn parse_flow_step_simple(&mut self, _kw: &str) -> Result<(Loc, String), ParseError> {
4142        let tok = self.current().clone();
4143        self.advance(); // consume keyword
4144        let target = if self.at_declaration_start()
4145            || self.check(TokenType::RBrace)
4146            || self.check(TokenType::Eof)
4147        {
4148            String::new()
4149        } else {
4150            self.consume_any_ident_or_kw()?.value.clone()
4151        };
4152        // Skip optional braced block
4153        if self.check(TokenType::LBrace) {
4154            self.skip_braced_block()?;
4155        }
4156        Ok((
4157            Loc {
4158                line: tok.line,
4159                column: tok.column,
4160            },
4161            target,
4162        ))
4163    }
4164
4165    /// Parse: keyword { ... } — block-level step, skip body structurally.
4166    /// §Fase 111.e — `stream { <steps> }` with a REAL body.
4167    ///
4168    /// The four block primitives (`deliberate`, `consensus`, `stream`,
4169    /// `transact`) all went through [`Self::parse_block_step`], whose entire job
4170    /// is `skip_braced_block()`. Their bodies were discarded at parse time — so
4171    /// their handlers were not no-ops through neglect, they were no-ops
4172    /// *by construction*: there was nothing in the AST to execute. §111 retracted
4173    /// `transact`; this gives `stream` its body back. `deliberate` / `consensus`
4174    /// remain body-less pending their Tier-4 disposition.
4175    fn parse_stream_block(&mut self) -> Result<StreamBlock, ParseError> {
4176        let tok = self.current().clone();
4177        let loc = self.loc_of(&tok);
4178        self.advance(); // consume `stream`
4179
4180        // Tolerate the pre-111 form `stream <effect-ish tokens> { … }`: skip any
4181        // argument tokens ahead of the brace, exactly as `parse_block_step` did,
4182        // so an existing program keeps parsing. Only the BODY changes.
4183        while !self.check(TokenType::LBrace)
4184            && !self.check(TokenType::RBrace)
4185            && !self.check(TokenType::Eof)
4186            && !self.at_declaration_start()
4187        {
4188            self.advance();
4189        }
4190
4191        let mut body = Vec::new();
4192        if self.check(TokenType::LBrace) {
4193            self.advance();
4194            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4195                body.push(self.parse_flow_step()?);
4196            }
4197            self.consume(TokenType::RBrace)?;
4198        }
4199
4200        Ok(StreamBlock { body, loc })
4201    }
4202
4203    fn parse_block_step(&mut self, _kw: &str) -> Result<Loc, ParseError> {
4204        let tok = self.current().clone();
4205        self.advance();
4206        // Skip optional arguments before brace
4207        while !self.check(TokenType::LBrace)
4208            && !self.check(TokenType::RBrace)
4209            && !self.check(TokenType::Eof)
4210            && !self.at_declaration_start()
4211        {
4212            self.advance();
4213        }
4214        if self.check(TokenType::LBrace) {
4215            self.skip_braced_block()?;
4216        }
4217        Ok(Loc {
4218            line: tok.line,
4219            column: tok.column,
4220        })
4221    }
4222
4223    /// §Fase 86 — parse `forge <Name>(seed: "<text>") -> <Type> { mode:,
4224    /// novelty:, depth:, branches:, constraints: }`. Real field capture
4225    /// (replacing the pre-§86 discard-everything stub). Strict closed-catalog:
4226    /// an unknown field is a hard parse error; all cross-field laws (Boden mode
4227    /// catalog, novelty range, depth/branches ≥ 1, `constraints:` → `anchor`)
4228    /// are §86.c type-checker territory.
4229    fn parse_forge_step(&mut self) -> Result<ForgeBlock, ParseError> {
4230        let tok = self.consume(TokenType::Forge)?;
4231        let name = self.consume(TokenType::Identifier)?.value;
4232        let mut node = ForgeBlock {
4233            name,
4234            novelty: 0.5,
4235            depth: 1,
4236            branches: 1,
4237            loc: Loc { line: tok.line, column: tok.column },
4238            ..Default::default()
4239        };
4240        // `(seed: "...")`
4241        self.consume(TokenType::LParen)?;
4242        let arg = self.consume_any_ident_or_kw()?.value;
4243        self.consume(TokenType::Colon)?;
4244        if arg != "seed" {
4245            return Err(self.error(&format!(
4246                "forge '{}' expects `seed:` as its argument, found `{arg}`",
4247                node.name
4248            )));
4249        }
4250        node.seed = self.consume(TokenType::StringLit)?.value;
4251        self.consume(TokenType::RParen)?;
4252        // `-> <Type>`
4253        self.consume(TokenType::Arrow)?;
4254        node.output_type = self.consume_any_ident_or_kw()?.value;
4255        // `{ fields }`
4256        self.consume(TokenType::LBrace)?;
4257        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4258            let field = self.consume_any_ident_or_kw()?.value;
4259            self.consume(TokenType::Colon)?;
4260            match field.as_str() {
4261                "mode" => node.mode = self.consume_any_ident_or_kw()?.value,
4262                "novelty" => node.novelty = self.consume_number()?,
4263                "depth" => {
4264                    node.depth = self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0)
4265                }
4266                "branches" => {
4267                    node.branches =
4268                        self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0)
4269                }
4270                "constraints" => node.constraints_ref = self.consume_any_ident_or_kw()?.value,
4271                other => {
4272                    return Err(self.error(&format!("unknown forge field `{other}`")))
4273                }
4274            }
4275            if self.check(TokenType::Comma) {
4276                self.consume(TokenType::Comma)?;
4277            }
4278        }
4279        self.consume(TokenType::RBrace)?;
4280        Ok(node)
4281    }
4282
4283    /// §Fase 65 — Parse `par { stmt1  stmt2  … }` into CONCURRENT branches.
4284    /// Each top-level flow statement inside the block is one branch (a
4285    /// single-statement body); they execute concurrently at runtime
4286    /// (`flow_dispatcher::parallel::run_branches_concurrently`). Before §65 the
4287    /// `par` body was skipped (`parse_block_step`), so the branches were lost
4288    /// and the handler ran as a stub. Multi-statement branches (grouping
4289    /// several steps into one sequential branch) are a future grammar
4290    /// extension; today the natural `par { step A  step B }` fans A and B out.
4291    fn parse_par_block(&mut self) -> Result<ParBlock, ParseError> {
4292        let tok = self.current().clone();
4293        self.advance(); // consume `par`
4294        self.consume(TokenType::LBrace)?;
4295        let mut branches: Vec<Vec<FlowStep>> = Vec::new();
4296        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4297            branches.push(vec![self.parse_flow_step()?]);
4298        }
4299        self.consume(TokenType::RBrace)?;
4300        Ok(ParBlock {
4301            branches,
4302            loc: Loc {
4303                line: tok.line,
4304                column: tok.column,
4305            },
4306        })
4307    }
4308
4309    /// §Fase 51.a — Parse the `quant` cognitive block surface.
4310    ///
4311    /// Grammar (the attribute header is OPTIONAL):
4312    /// ```text
4313    /// quant { <flow steps> }
4314    /// quant(encoding: amplitude, observable: M, qubits: 10,
4315    ///       depth: 4, bandwidth: 0.5, reupload: 3, backend: quant_sim) { <flow steps> }
4316    /// ```
4317    /// The bare form (the paper's example) leaves every attribute defaulted
4318    /// (`encoding = amplitude`, `effect = quant_sim`). The body is parsed into
4319    /// real nested `FlowStep`s — like `par` branches — so §51.b's Continuous
4320    /// Type Invariant scans actual AST rather than skipped tokens.
4321    /// §Fase 88.a — parse `warden(<target>) within <Scope> { <body> }`. The
4322    /// `within <Scope>` clause is MANDATORY at the grammar level (fail-closed by
4323    /// construction: a scopeless warden cannot be written); §88.c checks the
4324    /// scope RESOLVES + the target is in its allowlist.
4325    fn parse_warden(&mut self) -> Result<WardenBlock, ParseError> {
4326        let tok = self.consume(TokenType::Warden)?;
4327        // `(<target>)` — the resource under analysis.
4328        self.consume(TokenType::LParen)?;
4329        let target = self.consume_any_ident_or_kw()?.value;
4330        self.consume(TokenType::RParen)?;
4331        // `within <Scope>` — MANDATORY. Omitting it is a hard parse error.
4332        self.consume(TokenType::Within)?;
4333        let scope_ref = self.consume(TokenType::Identifier)?.value;
4334        let mut block = WardenBlock {
4335            target,
4336            scope_ref,
4337            body: Vec::new(),
4338            loc: Loc {
4339                line: tok.line,
4340                column: tok.column,
4341            },
4342        };
4343        // Body: real nested flow steps (like `quant`/`par`).
4344        self.consume(TokenType::LBrace)?;
4345        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4346            block.body.push(self.parse_flow_step()?);
4347        }
4348        self.consume(TokenType::RBrace)?;
4349        Ok(block)
4350    }
4351
4352    /// §Fase 88.a — parse `scope <Name> { targets: [ … ], depth: <ident>,
4353    /// approver: [requires] "<cap>" }`. Flat key:value block (the `cache` shape).
4354    /// Catalog + non-empty validation is §88.c. Unknown fields are a hard error
4355    /// (D83.7): a scope governs an offensive-capable analysis.
4356    fn parse_scope(&mut self) -> Result<ScopeDefinition, ParseError> {
4357        let tok = self.consume(TokenType::Scope)?;
4358        let name = self.consume(TokenType::Identifier)?.value;
4359        let mut node = ScopeDefinition {
4360            name,
4361            loc: Loc {
4362                line: tok.line,
4363                column: tok.column,
4364            },
4365            ..Default::default()
4366        };
4367        self.consume(TokenType::LBrace)?;
4368        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4369            let key = self.consume_any_ident_or_kw()?.value;
4370            self.consume(TokenType::Colon)?;
4371            match key.as_str() {
4372                "targets" => {
4373                    self.consume(TokenType::LBracket)?;
4374                    while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
4375                        let t = if self.check(TokenType::StringLit) {
4376                            self.consume(TokenType::StringLit)?.value
4377                        } else {
4378                            self.consume_any_ident_or_kw()?.value
4379                        };
4380                        node.targets.push(t);
4381                        if self.check(TokenType::Comma) {
4382                            self.advance();
4383                        }
4384                    }
4385                    self.consume(TokenType::RBracket)?;
4386                }
4387                "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
4388                "approver" => {
4389                    // Optional `requires` sugar before the capability string.
4390                    if self.current().value == "requires" {
4391                        self.advance();
4392                    }
4393                    node.approver = self.consume(TokenType::StringLit)?.value;
4394                }
4395                other => {
4396                    return Err(self.error(&format!(
4397                        "unknown scope field `{other}` in scope `{}` — expected \
4398                         `targets` / `depth` / `approver`",
4399                        node.name
4400                    )))
4401                }
4402            }
4403            if self.check(TokenType::Comma) {
4404                self.consume(TokenType::Comma)?;
4405            }
4406        }
4407        self.consume(TokenType::RBrace)?;
4408        Ok(node)
4409    }
4410
4411    fn parse_quant(&mut self) -> Result<QuantBlock, ParseError> {
4412        let tok = self.current().clone();
4413        self.advance(); // consume `quant`
4414
4415        let mut block = QuantBlock {
4416            encoding: None,
4417            observable: None,
4418            qubits: None,
4419            depth: None,
4420            bandwidth: None,
4421            reupload: None,
4422            // D1/D9 default backend: the CPU simulator effect. `qpu_native` is
4423            // opt-in via `backend: qpu_native`.
4424            effect: "quant_sim".to_string(),
4425            body: Vec::new(),
4426            loc: Loc {
4427                line: tok.line,
4428                column: tok.column,
4429            },
4430        };
4431
4432        // ── Optional attribute header: `(key: value, …)` ──
4433        if self.check(TokenType::LParen) {
4434            self.advance();
4435            while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
4436                let key = self.consume_any_ident_or_kw()?.value;
4437                self.consume(TokenType::Colon)?;
4438                match key.as_str() {
4439                    "encoding" => {
4440                        block.encoding = Some(self.consume_any_ident_or_kw()?.value)
4441                    }
4442                    "observable" => {
4443                        block.observable = Some(self.parse_dotted_identifier()?)
4444                    }
4445                    "qubits" => block.qubits = Some(self.consume_number()? as i64),
4446                    "depth" => block.depth = Some(self.consume_number()? as i64),
4447                    "bandwidth" => block.bandwidth = Some(self.consume_number()?),
4448                    // §Fase 69.c — data re-uploading layers.
4449                    "reupload" => block.reupload = Some(self.consume_number()? as i64),
4450                    // `backend:` selects the algebraic-effect tag (D1/D9).
4451                    "backend" => block.effect = self.consume_any_ident_or_kw()?.value,
4452                    other => {
4453                        return Err(ParseError {
4454                            message: format!(
4455                                "Unknown `quant` attribute `{other}` — expected one of \
4456                                 encoding, observable, qubits, depth, bandwidth, reupload, backend"
4457                            ),
4458                            line: self.current().line,
4459                            column: self.current().column,
4460                            ..Default::default()
4461                        });
4462                    }
4463                }
4464                // Optional comma between attributes (order-free, trailing-comma ok).
4465                if self.check(TokenType::Comma) {
4466                    self.advance();
4467                }
4468            }
4469            self.consume(TokenType::RParen)?;
4470        }
4471
4472        // ── Body: real nested flow steps (like `par`) ──
4473        self.consume(TokenType::LBrace)?;
4474        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4475            block.body.push(self.parse_flow_step()?);
4476        }
4477        self.consume(TokenType::RBrace)?;
4478
4479        Ok(block)
4480    }
4481
4482    /// §Fase 51.d.2 — Parse the `yield <expr>` measurement point. Reuses the
4483    /// `let`-value expression grammar (reference / literal / arithmetic) so the
4484    /// yielded value's tokenization intent is preserved in `value_kind`.
4485    fn parse_yield(&mut self) -> Result<YieldStatement, ParseError> {
4486        let tok = self.consume(TokenType::Yield)?;
4487        let loc = self.loc_of(&tok);
4488        self.last_let_value_kind = "literal".to_string();
4489        let value_expr = self.parse_let_value_expr()?;
4490        Ok(YieldStatement {
4491            value_expr,
4492            value_kind: self.last_let_value_kind.clone(),
4493            loc,
4494        })
4495    }
4496
4497    /// Parse: keyword Name on target -> output_type (apply pattern).
4498    /// §Fase 111.f — `compute <Name> on <a>, <b>, … -> <out>`.
4499    ///
4500    /// Positional arguments, bound to the compute's declared parameters in order.
4501    /// The generic [`Self::parse_apply_step`] captured a single `on <target>` and
4502    /// then the call site threw even that away (`arguments: Vec::new()`).
4503    fn parse_compute_apply(&mut self) -> Result<ComputeApplyStep, ParseError> {
4504        let tok = self.current().clone();
4505        let loc = self.loc_of(&tok);
4506        self.advance(); // consume `compute`
4507        let compute_name = self.consume_any_ident_or_kw()?.value.clone();
4508
4509        let mut arguments = Vec::new();
4510        if self.current().value == "on" {
4511            self.advance();
4512            loop {
4513                arguments.push(self.consume_any_ident_or_kw()?.value.clone());
4514                if self.check(TokenType::Comma) {
4515                    self.advance();
4516                } else {
4517                    break;
4518                }
4519            }
4520        }
4521
4522        let mut output_name = String::new();
4523        if self.check(TokenType::Arrow) {
4524            self.advance();
4525            output_name = self.consume_any_ident_or_kw()?.value.clone();
4526        }
4527
4528        Ok(ComputeApplyStep {
4529            compute_name,
4530            arguments,
4531            output_name,
4532            loc,
4533        })
4534    }
4535
4536    fn parse_apply_step(&mut self, _kw: &str) -> Result<(Loc, String, String, String), ParseError> {
4537        let tok = self.current().clone();
4538        self.advance(); // consume keyword
4539        let name = self.consume_any_ident_or_kw()?.value.clone();
4540        let mut target = String::new();
4541        let mut output_type = String::new();
4542        // "on" target
4543        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4544            let next = self.current().clone();
4545            if next.value == "on" {
4546                self.advance();
4547                target = self.consume_any_ident_or_kw()?.value.clone();
4548            }
4549        }
4550        // -> output_type
4551        if self.check(TokenType::Arrow) {
4552            self.advance();
4553            output_type = self.consume_any_ident_or_kw()?.value.clone();
4554        }
4555        // Skip optional braced block
4556        if self.check(TokenType::LBrace) {
4557            self.skip_braced_block()?;
4558        }
4559        Ok((
4560            Loc {
4561                line: tok.line,
4562                column: tok.column,
4563            },
4564            name,
4565            target,
4566            output_type,
4567        ))
4568    }
4569
4570    fn parse_weave_step(&mut self) -> Result<FlowStep, ParseError> {
4571        let tok = self.current().clone();
4572        self.advance();
4573        let mut node = WeaveStep {
4574            sources: Vec::new(),
4575            target: String::new(),
4576            format_type: String::new(),
4577            priority: Vec::new(),
4578            style: String::new(),
4579            loc: Loc {
4580                line: tok.line,
4581                column: tok.column,
4582            },
4583        };
4584        if self.check(TokenType::LBrace) {
4585            self.advance();
4586            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4587                let f = self.current().value.clone();
4588                self.advance();
4589                if self.check(TokenType::Colon) {
4590                    self.advance();
4591                    match f.as_str() {
4592                        "sources" => node.sources = self.parse_bracketed_identifiers()?,
4593                        "target" => node.target = self.consume_any_ident_or_kw()?.value.clone(),
4594                        "format" => {
4595                            node.format_type = self.consume_any_ident_or_kw()?.value.clone()
4596                        }
4597                        "priority" => node.priority = self.parse_bracketed_identifiers()?,
4598                        "style" => node.style = self.consume_any_ident_or_kw()?.value.clone(),
4599                        _ => self.skip_value(),
4600                    }
4601                }
4602            }
4603            if self.check(TokenType::RBrace) {
4604                self.advance();
4605            }
4606        }
4607        Ok(FlowStep::Weave(node))
4608    }
4609
4610    fn parse_use_step(&mut self) -> Result<FlowStep, ParseError> {
4611        let tok = self.current().clone();
4612        self.advance();
4613        let tool_name = self.consume_any_ident_or_kw()?.value.clone();
4614        // §Fase 58.b — two mutually-exclusive `use` argument surfaces:
4615        //   * `use Tool(query = "${q}", max_results = 5)` — D2 canonical
4616        //     multi-field keyword args (§58.b `UseArgs::Named`).
4617        //   * `use Tool on "${arg}"` / `on query` — the §54.b single positional
4618        //     argument (D5 back-compat, `UseArgs::LegacyPositional`):
4619        //       - a STRING LITERAL carrying interpolation (`on "${query}"`)
4620        //         resolved at dispatch against request-bound flow params;
4621        //       - a BARE identifier / literal (`on query` / `on 42`) verbatim.
4622        //     (Unquoted `${query}` is intentionally NOT a form — interpolation
4623        //     lives inside string literals everywhere in Axon.)
4624        let args = if self.check(TokenType::LParen) {
4625            UseArgs::Named(self.parse_named_arg_list()?)
4626        } else {
4627            let mut argument = String::new();
4628            if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4629                let next = self.current().clone();
4630                if next.value == "on" {
4631                    self.advance();
4632                    argument = self.consume_any_ident_or_kw()?.value.clone();
4633                }
4634            }
4635            UseArgs::LegacyPositional(argument)
4636        };
4637        if self.check(TokenType::LBrace) {
4638            self.skip_braced_block()?;
4639        }
4640        Ok(FlowStep::UseTool(UseToolStep {
4641            tool_name,
4642            args,
4643            loc: Loc {
4644                line: tok.line,
4645                column: tok.column,
4646            },
4647        }))
4648    }
4649
4650    /// §Fase 58.b — parse `(name = value, …)` keyword args for the canonical
4651    /// `use Tool(...)` multi-field dispatch. Values are captured as expression
4652    /// strings (StringLit / Integer / Float / Bool / dotted identifier / list)
4653    /// via the shared `parse_let_atom`, since the frontend has no structured
4654    /// `Expr`. A trailing comma is tolerated; `()` yields no args.
4655    fn parse_named_arg_list(&mut self) -> Result<Vec<(String, String, String)>, ParseError> {
4656        self.consume(TokenType::LParen)?;
4657        let mut args = Vec::new();
4658        while !self.check(TokenType::RParen) {
4659            // Accept a keyword-as-name (`filter`, `type`, `from`, …) — real
4660            // adopter schemas use such names; the following `=` disambiguates.
4661            let name = self.consume_any_ident_or_kw()?.value;
4662            self.consume(TokenType::Assign)?;
4663            let value = self.parse_let_atom()?;
4664            // §Fase 60 — `parse_let_atom` classified the value (`"literal"` vs
4665            // `"reference"`); carry it so the runtime resolves a bare
4666            // identifier / `Step.output` as a binding lookup, not a literal.
4667            let value_kind = self.last_let_value_kind.clone();
4668            args.push((name, value, value_kind));
4669            if self.check(TokenType::Comma) {
4670                self.advance();
4671            } else {
4672                break;
4673            }
4674        }
4675        self.consume(TokenType::RParen)?;
4676        Ok(args)
4677    }
4678
4679    fn parse_remember_step(&mut self) -> Result<FlowStep, ParseError> {
4680        let tok = self.current().clone();
4681        self.advance();
4682        let expr = self.consume_any_ident_or_kw()?.value.clone();
4683        let mut mem = String::new();
4684        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4685            let next = self.current().clone();
4686            if next.value == "in" || next.ttype == TokenType::In {
4687                self.advance();
4688                mem = self.consume_any_ident_or_kw()?.value.clone();
4689            }
4690        }
4691        Ok(FlowStep::Remember(RememberStep {
4692            expression: expr,
4693            memory_target: mem,
4694            loc: Loc {
4695                line: tok.line,
4696                column: tok.column,
4697            },
4698        }))
4699    }
4700
4701    fn parse_recall_step(&mut self) -> Result<FlowStep, ParseError> {
4702        let tok = self.current().clone();
4703        self.advance();
4704        let query = if self.check(TokenType::StringLit) {
4705            self.consume(TokenType::StringLit)?.value.clone()
4706        } else {
4707            self.consume_any_ident_or_kw()?.value.clone()
4708        };
4709        let mut mem = String::new();
4710        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4711            let next = self.current().clone();
4712            if next.value == "from" || next.ttype == TokenType::From {
4713                self.advance();
4714                mem = self.consume_any_ident_or_kw()?.value.clone();
4715            }
4716        }
4717        Ok(FlowStep::Recall(RecallStep {
4718            query,
4719            memory_source: mem,
4720            loc: Loc {
4721                line: tok.line,
4722                column: tok.column,
4723            },
4724        }))
4725    }
4726
4727    fn parse_hibernate_step(&mut self) -> Result<FlowStep, ParseError> {
4728        let tok = self.current().clone();
4729        self.advance();
4730        let mut event = String::new();
4731        let mut timeout = String::new();
4732        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4733            event = self.consume_any_ident_or_kw()?.value.clone();
4734        }
4735        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4736            let next = self.current().clone();
4737            if next.ttype == TokenType::Duration {
4738                self.advance();
4739                timeout = next.value.clone();
4740            }
4741        }
4742        Ok(FlowStep::Hibernate(HibernateStep {
4743            event_name: event,
4744            timeout,
4745            loc: Loc {
4746                line: tok.line,
4747                column: tok.column,
4748            },
4749        }))
4750    }
4751
4752    /// §Fase 108.d — `focus <Dataspace> { where: "<filter>", select: [cols], as: <name> }`
4753    /// — σ_φ ∘ π_v over a declared dataspace. The `where:` string is the
4754    /// §35 data-plane filter grammar (D108.9, shared with retrieve /
4755    /// navigate). Pre-108.d the optional body was silently discarded.
4756    /// §Fase 109.a — `grad <letName> wrt <x> [as <name>]` /
4757    /// `grad <letName> wrt [a, b] as <name>`. The differentiation itself
4758    /// happens at CHECK/IR time (T931/T932 + the symbolic differentiator);
4759    /// the parser only captures the surface.
4760    fn parse_grad_step(&mut self) -> Result<FlowStep, ParseError> {
4761        let tok = self.current().clone();
4762        self.advance();
4763        let target = self.consume_any_ident_or_kw()?.value.clone();
4764        let mut wrt: Vec<String> = Vec::new();
4765        let mut output = String::new();
4766        if !self.at_declaration_start() && self.current().value == "wrt" {
4767            self.advance();
4768            if self.check(TokenType::LBracket) {
4769                wrt = self.parse_bracketed_identifiers()?;
4770            } else {
4771                wrt.push(self.consume_any_ident_or_kw()?.value.clone());
4772            }
4773        }
4774        if !self.at_declaration_start() && self.current().value == "as" {
4775            self.advance();
4776            output = self.consume_any_ident_or_kw()?.value.clone();
4777        }
4778        Ok(FlowStep::Grad(GradStep {
4779            target,
4780            wrt,
4781            output,
4782            loc: Loc {
4783                line: tok.line,
4784                column: tok.column,
4785            },
4786        }))
4787    }
4788
4789    fn parse_focus_step(&mut self) -> Result<FlowStep, ParseError> {
4790        let tok = self.current().clone();
4791        self.advance();
4792        let expression = if self.at_declaration_start()
4793            || self.check(TokenType::RBrace)
4794            || self.check(TokenType::Eof)
4795        {
4796            String::new()
4797        } else {
4798            self.consume_any_ident_or_kw()?.value.clone()
4799        };
4800        let mut where_expr = String::new();
4801        let mut select: Vec<String> = Vec::new();
4802        let mut output = String::new();
4803        if self.check(TokenType::LBrace) {
4804            self.advance();
4805            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4806                if self.check(TokenType::Comma) {
4807                    self.advance();
4808                    continue;
4809                }
4810                let f = self.current().value.clone();
4811                self.advance();
4812                if self.check(TokenType::Colon) {
4813                    self.advance();
4814                    match f.as_str() {
4815                        "where" => {
4816                            where_expr = self.consume(TokenType::StringLit)?.value.clone()
4817                        }
4818                        "select" => select = self.parse_bracketed_identifiers()?,
4819                        "as" | "alias" => {
4820                            output = self.consume_any_ident_or_kw()?.value.clone()
4821                        }
4822                        _ => self.skip_value(),
4823                    }
4824                }
4825            }
4826            if self.check(TokenType::RBrace) {
4827                self.advance();
4828            }
4829        }
4830        Ok(FlowStep::Focus(FocusStep {
4831            expression,
4832            where_expr,
4833            select,
4834            output,
4835            loc: Loc {
4836                line: tok.line,
4837                column: tok.column,
4838            },
4839        }))
4840    }
4841
4842    fn parse_associate_step(&mut self) -> Result<FlowStep, ParseError> {
4843        let tok = self.current().clone();
4844        self.advance();
4845        let left = self.consume_any_ident_or_kw()?.value.clone();
4846        let mut right = String::new();
4847        let mut using = String::new();
4848        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4849            right = self.consume_any_ident_or_kw()?.value.clone();
4850        }
4851        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4852            let next = self.current().clone();
4853            if next.value == "using" {
4854                self.advance();
4855                using = self.consume_any_ident_or_kw()?.value.clone();
4856            }
4857        }
4858        let mut output = String::new();
4859        if self.check(TokenType::LBrace) {
4860            self.advance();
4861            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4862                let f = self.current().value.clone();
4863                self.advance();
4864                if self.check(TokenType::Colon) {
4865                    self.advance();
4866                    match f.as_str() {
4867                        "as" | "alias" => output = self.consume_any_ident_or_kw()?.value.clone(),
4868                        _ => self.skip_value(),
4869                    }
4870                }
4871            }
4872            if self.check(TokenType::RBrace) {
4873                self.advance();
4874            }
4875        }
4876        Ok(FlowStep::Associate(AssociateStep {
4877            left,
4878            right,
4879            using_field: using,
4880            output,
4881            loc: Loc {
4882                line: tok.line,
4883                column: tok.column,
4884            },
4885        }))
4886    }
4887
4888    fn parse_aggregate_step(&mut self) -> Result<FlowStep, ParseError> {
4889        let tok = self.current().clone();
4890        self.advance();
4891        let target = self.consume_any_ident_or_kw()?.value.clone();
4892        let mut group_by = Vec::new();
4893        let mut alias = String::new();
4894        let mut compute: Vec<String> = Vec::new();
4895        let mut where_expr = String::new();
4896        if self.check(TokenType::LBrace) {
4897            self.advance();
4898            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4899                let f = self.current().value.clone();
4900                self.advance();
4901                if self.check(TokenType::Colon) {
4902                    self.advance();
4903                    match f.as_str() {
4904                        "group_by" => group_by = self.parse_bracketed_identifiers()?,
4905                        "alias" | "as" => alias = self.consume_any_ident_or_kw()?.value.clone(),
4906                        // §Fase 108.d — the closed aggregate catalog, kept
4907                        // RAW (`count`, `sum(score)`, …); T930 validates.
4908                        "compute" => compute = self.parse_bracketed_aggregates()?,
4909                        // §Fase 108.d — the data-plane where (D108.9).
4910                        "where" => where_expr = self.consume(TokenType::StringLit)?.value.clone(),
4911                        _ => self.skip_value(),
4912                    }
4913                }
4914            }
4915            if self.check(TokenType::RBrace) {
4916                self.advance();
4917            }
4918        }
4919        Ok(FlowStep::Aggregate(AggregateStep {
4920            target,
4921            group_by,
4922            alias,
4923            compute,
4924            where_expr,
4925            loc: Loc {
4926                line: tok.line,
4927                column: tok.column,
4928            },
4929        }))
4930    }
4931
4932    fn parse_explore_step(&mut self) -> Result<FlowStep, ParseError> {
4933        let tok = self.current().clone();
4934        self.advance();
4935        let target = self.consume_any_ident_or_kw()?.value.clone();
4936        let mut limit = None;
4937        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
4938            if self.current().ttype == TokenType::Integer {
4939                limit = self.current().value.parse::<i64>().ok();
4940                self.advance();
4941            }
4942        }
4943        let mut output = String::new();
4944        if self.check(TokenType::LBrace) {
4945            self.advance();
4946            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
4947                let f = self.current().value.clone();
4948                self.advance();
4949                if self.check(TokenType::Colon) {
4950                    self.advance();
4951                    match f.as_str() {
4952                        "as" | "alias" => output = self.consume_any_ident_or_kw()?.value.clone(),
4953                        _ => self.skip_value(),
4954                    }
4955                }
4956            }
4957            if self.check(TokenType::RBrace) {
4958                self.advance();
4959            }
4960        }
4961        Ok(FlowStep::ExploreStep(ExploreStepNode {
4962            target,
4963            limit,
4964            output,
4965            loc: Loc {
4966                line: tok.line,
4967                column: tok.column,
4968            },
4969        }))
4970    }
4971
4972    /// §Fase 108.d — parse `[count, sum(score), avg(x)]`: bracketed
4973    /// aggregate entries, each `ident` or `ident(ident)`, kept raw.
4974    fn parse_bracketed_aggregates(&mut self) -> Result<Vec<String>, ParseError> {
4975        let mut out = Vec::new();
4976        self.consume(TokenType::LBracket)?;
4977        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
4978            let name = self.consume_any_ident_or_kw()?.value.clone();
4979            if self.check(TokenType::LParen) {
4980                self.advance();
4981                let col = self.consume_any_ident_or_kw()?.value.clone();
4982                self.consume(TokenType::RParen)?;
4983                out.push(format!("{name}({col})"));
4984            } else {
4985                out.push(name);
4986            }
4987            if self.check(TokenType::Comma) {
4988                self.advance();
4989            }
4990        }
4991        self.consume(TokenType::RBracket)?;
4992        Ok(out)
4993    }
4994
4995    /// §Fase 108.c — the governed ingest step:
4996    ///
4997    /// ```text
4998    /// ingest <sourceRef> into <Dataspace> {
4999    ///     format: csv | json
5000    ///     limits { max_bytes: N, max_rows: N }
5001    /// }
5002    /// ```
5003    ///
5004    /// Until 108.c the body was consumed by `skip_braced_block()`. Now it
5005    /// is a closed grammar: `format:` (raw here; required + validated by
5006    /// `axon-T929`) and an optional `limits { … }` block whose bounds are
5007    /// enforced on the raw byte stream BEFORE parsing (§100). An unknown
5008    /// body entry is a parse error.
5009    fn parse_ingest_step(&mut self) -> Result<FlowStep, ParseError> {
5010        let tok = self.current().clone();
5011        self.advance();
5012        let source = self.consume_any_ident_or_kw()?.value.clone();
5013        let mut target = String::new();
5014        let mut format = String::new();
5015        let mut max_bytes: Option<u64> = None;
5016        let mut max_rows: Option<u64> = None;
5017        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
5018            let next = self.current().clone();
5019            if next.value == "into" || next.ttype == TokenType::Into {
5020                self.advance();
5021                target = self.consume_any_ident_or_kw()?.value.clone();
5022            }
5023        }
5024        if self.check(TokenType::LBrace) {
5025            self.consume(TokenType::LBrace)?;
5026            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5027                // Optional separators between body entries.
5028                if self.check(TokenType::Comma) {
5029                    self.advance();
5030                    continue;
5031                }
5032                let entry = self.current().clone();
5033                match entry.value.as_str() {
5034                    "format" => {
5035                        self.advance();
5036                        self.consume(TokenType::Colon)?;
5037                        format = self.consume_any_ident_or_kw()?.value.clone();
5038                    }
5039                    "limits" => {
5040                        self.advance();
5041                        self.consume(TokenType::LBrace)?;
5042                        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5043                            let bound = self.current().clone();
5044                            self.advance();
5045                            self.consume(TokenType::Colon)?;
5046                            let num_tok = self.consume(TokenType::Integer)?.clone();
5047                            let value = num_tok.value.parse::<u64>().map_err(|_| ParseError {
5048                                message: format!(
5049                                    "ingest `limits` bound `{}` must be a non-negative \
5050                                     integer byte/row count, got `{}`.",
5051                                    bound.value, num_tok.value
5052                                ),
5053                                line: num_tok.line,
5054                                column: num_tok.column,
5055                                ..Default::default()
5056                            })?;
5057                            match bound.value.as_str() {
5058                                "max_bytes" => max_bytes = Some(value),
5059                                "max_rows" => max_rows = Some(value),
5060                                other => {
5061                                    return Err(ParseError {
5062                                        message: format!(
5063                                            "Unknown ingest limit `{other}`. The closed \
5064                                             limits grammar is `max_bytes: <N>` and \
5065                                             `max_rows: <N>` — bounds enforced on the raw \
5066                                             stream BEFORE parsing (§100).",
5067                                        ),
5068                                        line: bound.line,
5069                                        column: bound.column,
5070                                        ..Default::default()
5071                                    });
5072                                }
5073                            }
5074                            if self.check(TokenType::Comma) {
5075                                self.advance();
5076                            }
5077                        }
5078                        self.consume(TokenType::RBrace)?;
5079                    }
5080                    other => {
5081                        return Err(ParseError {
5082                            message: format!(
5083                                "Unknown entry `{other}` in ingest body. The closed \
5084                                 grammar is `format: csv|json` and \
5085                                 `limits {{ max_bytes: <N>, max_rows: <N> }}`.",
5086                            ),
5087                            line: entry.line,
5088                            column: entry.column,
5089                            ..Default::default()
5090                        });
5091                    }
5092                }
5093            }
5094            self.consume(TokenType::RBrace)?;
5095        }
5096        Ok(FlowStep::Ingest(IngestStep {
5097            source,
5098            target,
5099            format,
5100            max_bytes,
5101            max_rows,
5102            loc: Loc {
5103                line: tok.line,
5104                column: tok.column,
5105            },
5106        }))
5107    }
5108
5109    fn parse_navigate_step(&mut self) -> Result<FlowStep, ParseError> {
5110        let tok = self.current().clone();
5111        self.advance();
5112        let pix_name = self.consume_any_ident_or_kw()?.value.clone();
5113        let mut node = NavigateStep {
5114            pix_name,
5115            corpus_name: String::new(),
5116            query_expr: String::new(),
5117            trail_enabled: false,
5118            output_name: String::new(),
5119            seed: String::new(),
5120            budget: None,
5121            where_expr: String::new(),
5122            loc: Loc {
5123                line: tok.line,
5124                column: tok.column,
5125            },
5126        };
5127        if self.check(TokenType::LBrace) {
5128            self.advance();
5129            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5130                let f = self.current().value.clone();
5131                self.advance();
5132                if self.check(TokenType::Colon) {
5133                    self.advance();
5134                    match f.as_str() {
5135                        "corpus" => {
5136                            node.corpus_name = self.consume_any_ident_or_kw()?.value.clone()
5137                        }
5138                        "query" => {
5139                            node.query_expr = self.consume(TokenType::StringLit)?.value.clone()
5140                        }
5141                        "trail" => {
5142                            node.trail_enabled = self.consume_any_ident_or_kw()?.value == "true"
5143                        }
5144                        "output" | "as" => {
5145                            node.output_name = self.consume_any_ident_or_kw()?.value.clone()
5146                        }
5147                        // §Fase 63.B — MDN corpus-graph navigation.
5148                        "from" => node.seed = self.consume_any_ident_or_kw()?.value.clone(),
5149                        "budget" => node.budget = self.parse_optional_int(),
5150                        // §Fase 66 (Q2) — column-scoped navigation: a raw filter
5151                        // expr (mirrors `retrieve … where`) pushed to the SELECT
5152                        // that sources the corpus `documents:`/`relations:` rows,
5153                        // so a `corpus from axonstore` is scoped to a sub-tenant
5154                        // COLUMN (`where: "tenant_id == '${tenant_id}'"`), not just
5155                        // the axon-tenant RLS scope. Resolved by the §37.d filter
5156                        // compiler at runtime (`${name}` → `$N` bind params).
5157                        "where" => {
5158                            node.where_expr = self.consume(TokenType::StringLit)?.value.clone()
5159                        }
5160                        _ => self.skip_value(),
5161                    }
5162                }
5163            }
5164            if self.check(TokenType::RBrace) {
5165                self.advance();
5166            }
5167        }
5168        Ok(FlowStep::Navigate(node))
5169    }
5170
5171    fn parse_drill_step(&mut self) -> Result<FlowStep, ParseError> {
5172        let tok = self.current().clone();
5173        self.advance();
5174        let pix_name = self.consume_any_ident_or_kw()?.value.clone();
5175        let mut node = DrillStep {
5176            pix_name,
5177            subtree_path: String::new(),
5178            query_expr: String::new(),
5179            output_name: String::new(),
5180            loc: Loc {
5181                line: tok.line,
5182                column: tok.column,
5183            },
5184        };
5185        if self.check(TokenType::LBrace) {
5186            self.advance();
5187            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5188                let f = self.current().value.clone();
5189                self.advance();
5190                if self.check(TokenType::Colon) {
5191                    self.advance();
5192                    match f.as_str() {
5193                        "subtree" | "path" => {
5194                            node.subtree_path = self.consume(TokenType::StringLit)?.value.clone()
5195                        }
5196                        "query" => {
5197                            node.query_expr = self.consume(TokenType::StringLit)?.value.clone()
5198                        }
5199                        "output" | "as" => {
5200                            node.output_name = self.consume_any_ident_or_kw()?.value.clone()
5201                        }
5202                        _ => self.skip_value(),
5203                    }
5204                }
5205            }
5206            if self.check(TokenType::RBrace) {
5207                self.advance();
5208            }
5209        }
5210        Ok(FlowStep::Drill(node))
5211    }
5212
5213    fn parse_corroborate_step(&mut self) -> Result<FlowStep, ParseError> {
5214        let tok = self.current().clone();
5215        self.advance();
5216        let nav_ref = self.consume_any_ident_or_kw()?.value.clone();
5217        let mut output = String::new();
5218        if self.check(TokenType::Arrow) {
5219            self.advance();
5220            output = self.consume_any_ident_or_kw()?.value.clone();
5221        }
5222        Ok(FlowStep::Corroborate(CorroborateStep {
5223            navigate_ref: nav_ref,
5224            output_name: output,
5225            loc: Loc {
5226                line: tok.line,
5227                column: tok.column,
5228            },
5229        }))
5230    }
5231
5232    fn parse_listen_step(&mut self) -> Result<FlowStep, ParseError> {
5233        let tok = self.current().clone();
5234        self.advance();
5235        // §λ-L-E Fase 13 D4 — dual-mode listen:
5236        //   • String topic (legacy, deprecated since Fase 13)
5237        //   • Identifier (canonical: declared ChannelDefinition)
5238        let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
5239            (self.consume(TokenType::StringLit)?.value.clone(), false)
5240        } else {
5241            (self.consume_any_ident_or_kw()?.value.clone(), true)
5242        };
5243        let mut alias = String::new();
5244        if !self.at_declaration_start()
5245            && !self.check(TokenType::RBrace)
5246            && !self.check(TokenType::LBrace)
5247        {
5248            let next = self.current().clone();
5249            if next.value == "as" || next.ttype == TokenType::As {
5250                self.advance();
5251                alias = self.consume_any_ident_or_kw()?.value.clone();
5252            }
5253        }
5254        // §Fase 52.a — parse the handler body into real flow-steps (was
5255        // `skip_braced_block`'d, leaving the listener inert). The body runs on
5256        // each event / scheduled tick.
5257        let body = self.parse_listener_body()?;
5258        Ok(FlowStep::Listen(ListenStep {
5259            channel,
5260            channel_is_ref,
5261            event_alias: alias,
5262            body,
5263            loc: Loc {
5264                line: tok.line,
5265                column: tok.column,
5266            },
5267        }))
5268    }
5269
5270    /// §Fase 52.a — parse a `listen … { <flow steps> }` handler body. The body
5271    /// is OPTIONAL (a bodyless `listen channel` returns an empty Vec); when
5272    /// present, each statement is a real [`FlowStep`] (the same grammar as a
5273    /// flow / `quant` / `par` body), executed per trigger by the §52.c runtime.
5274    fn parse_listener_body(&mut self) -> Result<Vec<FlowStep>, ParseError> {
5275        let mut body = Vec::new();
5276        if self.check(TokenType::LBrace) {
5277            self.advance(); // consume `{`
5278            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5279                body.push(self.parse_flow_step()?);
5280            }
5281            self.consume(TokenType::RBrace)?;
5282        }
5283        Ok(body)
5284    }
5285
5286    fn parse_retrieve_step(&mut self) -> Result<FlowStep, ParseError> {
5287        let tok = self.current().clone();
5288        self.advance();
5289        let store = self.consume_any_ident_or_kw()?.value.clone();
5290        let mut where_expr = String::new();
5291        let mut alias = String::new();
5292        let mut order_by = String::new();
5293        let mut limit_expr = String::new();
5294        let mut aggregate = String::new();
5295        let mut group_by = String::new();
5296        let mut cache = String::new();
5297        if self.check(TokenType::LBrace) {
5298            self.advance();
5299            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5300                let f = self.current().value.clone();
5301                self.advance();
5302                if self.check(TokenType::Colon) {
5303                    self.advance();
5304                    match f.as_str() {
5305                        "where" => where_expr = self.consume(TokenType::StringLit)?.value.clone(),
5306                        "as" | "alias" => alias = self.consume_any_ident_or_kw()?.value.clone(),
5307                        // §Fase 67.b — `order_by:` is a string literal
5308                        // (`"col asc, col2 desc"`), same surface as `where:`.
5309                        "order_by" => {
5310                            order_by = self.consume(TokenType::StringLit)?.value.clone()
5311                        }
5312                        // §Fase 67.b — `limit:` is a bare integer literal
5313                        // (`limit: 100`) OR a string carrying a binding
5314                        // (`limit: "${max}"`). Captured raw; the runtime
5315                        // resolves + validates it as a `u32`.
5316                        "limit" => {
5317                            let t = self.current().clone();
5318                            match t.ttype {
5319                                TokenType::Integer | TokenType::StringLit => {
5320                                    limit_expr = t.value.clone();
5321                                    self.advance();
5322                                }
5323                                _ => self.skip_value(),
5324                            }
5325                        }
5326                        // §Fase 76.d — `aggregate:` is a string literal from
5327                        // the CLOSED catalog (`"count"`, `"sum(tokens)"`, …);
5328                        // `group_by:` is a string literal listing columns
5329                        // (`"industry, status"`). Both captured raw; the
5330                        // §38.d proof (axon-T843/T844/T845) + the runtime
5331                        // (`filter::parse_aggregate_clause`) validate.
5332                        "aggregate" => {
5333                            aggregate = self.consume(TokenType::StringLit)?.value.clone()
5334                        }
5335                        "group_by" => {
5336                            group_by = self.consume(TokenType::StringLit)?.value.clone()
5337                        }
5338                        // §Fase 85.b — `cache:` names a declared `cache`
5339                        // policy. A retrieve reads a store (never `pure`), so
5340                        // caching it always accepts staleness — the checker
5341                        // requires a finite `ttl:` on the referenced cache
5342                        // (axon-T865) and resolves the reference (axon-T864).
5343                        "cache" => cache = self.consume_any_ident_or_kw()?.value.clone(),
5344                        _ => self.skip_value(),
5345                    }
5346                }
5347            }
5348            if self.check(TokenType::RBrace) {
5349                self.advance();
5350            }
5351        }
5352        Ok(FlowStep::Retrieve(RetrieveStep {
5353            store_name: store,
5354            where_expr,
5355            alias,
5356            order_by,
5357            limit_expr,
5358            aggregate,
5359            group_by,
5360            cache,
5361            loc: Loc {
5362                line: tok.line,
5363                column: tok.column,
5364            },
5365        }))
5366    }
5367
5368    /// §Fase 35.m — Parse a `purge` step, capturing the optional
5369    /// `{ where: "<expr>" }` filter. (Fase 35.p moved `mutate` to its
5370    /// own `parse_mutate_step`, which also captures SET columns; this
5371    /// helper now serves `purge` alone — a `DELETE` has no SET clause.)
5372    ///
5373    /// Before Fase 35.m these two steps parsed via `parse_flow_step_simple`,
5374    /// which *skipped* the braced block — so a written `where:` clause
5375    /// was silently dropped and every `mutate`/`purge` ran against the
5376    /// whole store, leaving the entire Fase 35.b/c parameterized-filter
5377    /// machinery unreachable for them. This mirror of `parse_retrieve_step`
5378    /// (minus the `as:` alias — a mutate/purge binds no result) closes
5379    /// that gap. Returns `(loc, store_name, where_expr)`.
5380    fn parse_store_where_step(
5381        &mut self,
5382    ) -> Result<(Loc, String, String), ParseError> {
5383        let tok = self.current().clone();
5384        self.advance(); // consume the keyword
5385        let store = if self.at_declaration_start()
5386            || self.check(TokenType::RBrace)
5387            || self.check(TokenType::Eof)
5388        {
5389            String::new()
5390        } else {
5391            self.consume_any_ident_or_kw()?.value.clone()
5392        };
5393        let mut where_expr = String::new();
5394        if self.check(TokenType::LBrace) {
5395            self.advance();
5396            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5397                let field = self.current().value.clone();
5398                self.advance();
5399                if self.check(TokenType::Colon) {
5400                    self.advance();
5401                    match field.as_str() {
5402                        "where" => {
5403                            where_expr =
5404                                self.consume(TokenType::StringLit)?.value.clone()
5405                        }
5406                        _ => self.skip_value(),
5407                    }
5408                }
5409            }
5410            if self.check(TokenType::RBrace) {
5411                self.advance();
5412            }
5413        }
5414        Ok((
5415            Loc {
5416                line: tok.line,
5417                column: tok.column,
5418            },
5419            store,
5420            where_expr,
5421        ))
5422    }
5423
5424    /// §Fase 35.o — Parse a `persist` step, capturing the optional
5425    /// `{ col: value }` field block.
5426    ///
5427    /// Before Fase 35.o `persist` parsed via `parse_flow_step_simple`,
5428    /// which *skipped* the braced block — so a written field block was
5429    /// silently dropped and the runtime fell back to writing every
5430    /// context binding as a row, which fails against any real table
5431    /// (flows always carry more bindings than a table has columns).
5432    /// This captures the declared columns into `PersistStep.fields`;
5433    /// the runtime writes exactly those (interpolated). A `persist`
5434    /// with no block keeps the v1.30.0 user-bindings fallback — fully
5435    /// backward-compatible. Mirror of `parse_retrieve_step`, but the
5436    /// keys are arbitrary column names rather than the fixed
5437    /// `where:` / `as:` filter keys.
5438    ///
5439    /// The optional `into` connector (`persist into <store>`) is
5440    /// accepted and skipped — before Fase 35.o `into` was captured as
5441    /// the store name.
5442    fn parse_persist_step(&mut self) -> Result<FlowStep, ParseError> {
5443        let tok = self.current().clone();
5444        self.advance(); // consume `persist`
5445        // Optional `into` connector — skip it so the store name that
5446        // follows is not mistaken for the target.
5447        if self.current().value == "into" && !self.check(TokenType::LBrace) {
5448            self.advance();
5449        }
5450        let store = if self.at_declaration_start()
5451            || self.check(TokenType::LBrace)
5452            || self.check(TokenType::RBrace)
5453            || self.check(TokenType::Eof)
5454        {
5455            String::new()
5456        } else {
5457            self.consume_any_ident_or_kw()?.value.clone()
5458        };
5459        let mut fields: Vec<(String, String)> = Vec::new();
5460        if self.check(TokenType::LBrace) {
5461            self.advance();
5462            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5463                let col = self.current().value.clone();
5464                self.advance();
5465                if self.check(TokenType::Colon) {
5466                    self.advance();
5467                    let value = if self.check(TokenType::StringLit) {
5468                        self.consume(TokenType::StringLit)?.value.clone()
5469                    } else if self.check(TokenType::RBrace)
5470                        || self.check(TokenType::Eof)
5471                        || self.check(TokenType::Colon)
5472                    {
5473                        String::new()
5474                    } else {
5475                        let v = self.current().clone();
5476                        self.advance();
5477                        v.value.clone()
5478                    };
5479                    fields.push((col, value));
5480                }
5481            }
5482            if self.check(TokenType::RBrace) {
5483                self.advance();
5484            }
5485        }
5486        Ok(FlowStep::Persist(PersistStep {
5487            store_name: store,
5488            fields,
5489            loc: Loc {
5490                line: tok.line,
5491                column: tok.column,
5492            },
5493        }))
5494    }
5495
5496    /// §Fase 35.p — Parse a `mutate` step, capturing both the
5497    /// `{ where: "<expr>" }` filter AND the `{ col: value }` SET
5498    /// assignments.
5499    ///
5500    /// Before Fase 35.p `mutate` parsed via `parse_store_where_step`,
5501    /// which captured only `where:` and *skipped* every other key — so
5502    /// the runtime built the `UPDATE … SET` clause from every flow
5503    /// binding (params + step results + `let`s), which fails against
5504    /// any real table (`column "X" does not exist`). This closes the
5505    /// gap symmetrically to 35.o's `persist` block: every key other
5506    /// than `where:` is a SET column; a `mutate` with no SET column
5507    /// keeps the v1.31.0 user-bindings fallback. `where:` keeps its
5508    /// string-literal grammar (as in `retrieve` / `purge`).
5509    fn parse_mutate_step(&mut self) -> Result<FlowStep, ParseError> {
5510        let tok = self.current().clone();
5511        self.advance(); // consume `mutate`
5512        let store = if self.at_declaration_start()
5513            || self.check(TokenType::LBrace)
5514            || self.check(TokenType::RBrace)
5515            || self.check(TokenType::Eof)
5516        {
5517            String::new()
5518        } else {
5519            self.consume_any_ident_or_kw()?.value.clone()
5520        };
5521        let mut where_expr = String::new();
5522        let mut fields: Vec<(String, String)> = Vec::new();
5523        if self.check(TokenType::LBrace) {
5524            self.advance();
5525            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5526                let key = self.current().value.clone();
5527                self.advance();
5528                if self.check(TokenType::Colon) {
5529                    self.advance();
5530                    if key == "where" {
5531                        where_expr =
5532                            self.consume(TokenType::StringLit)?.value.clone();
5533                    } else {
5534                        let value = if self.check(TokenType::StringLit) {
5535                            self.consume(TokenType::StringLit)?.value.clone()
5536                        } else if self.check(TokenType::RBrace)
5537                            || self.check(TokenType::Eof)
5538                            || self.check(TokenType::Colon)
5539                        {
5540                            String::new()
5541                        } else {
5542                            let v = self.current().clone();
5543                            self.advance();
5544                            v.value.clone()
5545                        };
5546                        fields.push((key, value));
5547                    }
5548                }
5549            }
5550            if self.check(TokenType::RBrace) {
5551                self.advance();
5552            }
5553        }
5554        Ok(FlowStep::Mutate(MutateStep {
5555            store_name: store,
5556            where_expr,
5557            fields,
5558            loc: Loc {
5559                line: tok.line,
5560                column: tok.column,
5561            },
5562        }))
5563    }
5564
5565    // ── TIER 2 DECLARATIONS ────────────────────────────────────────
5566
5567    fn parse_agent(&mut self) -> Result<AgentDefinition, ParseError> {
5568        let tok = self.consume(TokenType::Agent)?;
5569        let name = self.consume(TokenType::Identifier)?.value;
5570        let mut node = AgentDefinition {
5571            name,
5572            goal: String::new(),
5573            tools: Vec::new(),
5574            memory_ref: String::new(),
5575            strategy: String::new(),
5576            on_stuck: String::new(),
5577            shield_ref: String::new(),
5578            max_iterations: None,
5579            max_tokens: None,
5580            max_time: String::new(),
5581            max_cost: None,
5582            loc: Loc {
5583                line: tok.line,
5584                column: tok.column,
5585            },
5586            leading_trivia: Vec::new(),
5587            trailing_trivia: Vec::new(),
5588        };
5589        // Skip optional parameters/return type before brace
5590        while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
5591            self.advance();
5592        }
5593        self.consume(TokenType::LBrace)?;
5594        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5595            let field = self.current().clone();
5596            let field_name = field.value.clone();
5597            self.advance();
5598            if self.check(TokenType::Colon) {
5599                self.advance();
5600                match field_name.as_str() {
5601                    "goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
5602                    "tools" => node.tools = self.parse_bracketed_identifiers()?,
5603                    "memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
5604                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
5605                    "on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
5606                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
5607                    "max_iterations" => node.max_iterations = self.parse_optional_int(),
5608                    "max_tokens" => node.max_tokens = self.parse_optional_int(),
5609                    "max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
5610                    "max_cost" => node.max_cost = self.parse_optional_float(),
5611                    _ => self.skip_value(),
5612                }
5613            } else if self.check(TokenType::LBrace) {
5614                self.skip_braced_block()?;
5615            }
5616        }
5617        self.consume(TokenType::RBrace)?;
5618        Ok(node)
5619    }
5620
5621    /// §Fase 53 — `extension Name { category: effects|scan, members: [ … ] }`.
5622    /// The parser is permissive on field/category VALUES (validated in
5623    /// §53.c by the type-checker — no-shadowing, category-membership);
5624    /// it only enforces the structural grammar here.
5625    fn parse_extension(&mut self) -> Result<ExtensionDefinition, ParseError> {
5626        let tok = self.consume(TokenType::Extension)?;
5627        let name = self.consume(TokenType::Identifier)?.value;
5628        let mut node = ExtensionDefinition {
5629            name,
5630            category: String::new(),
5631            members: Vec::new(),
5632            loc: Loc {
5633                line: tok.line,
5634                column: tok.column,
5635            },
5636            leading_trivia: Vec::new(),
5637            trailing_trivia: Vec::new(),
5638        };
5639        self.consume(TokenType::LBrace)?;
5640        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5641            let field_name = self.current().value.clone();
5642            self.advance();
5643            if self.check(TokenType::Colon) {
5644                self.advance();
5645                match field_name.as_str() {
5646                    "category" => {
5647                        node.category = self.consume_any_ident_or_kw()?.value.clone()
5648                    }
5649                    "members" => node.members = self.parse_extension_members()?,
5650                    _ => self.skip_value(),
5651                }
5652            } else if self.check(TokenType::LBrace) {
5653                self.skip_braced_block()?;
5654            }
5655        }
5656        self.consume(TokenType::RBrace)?;
5657        Ok(node)
5658    }
5659
5660    /// §Fase 53 — parse `[ "name" [ : { semantics: "…", default_confidence: 0.8 } ], … ]`.
5661    /// Each member is a string literal optionally followed by a metadata
5662    /// block. Trailing/interleaved commas are tolerated.
5663    fn parse_extension_members(&mut self) -> Result<Vec<ExtensionMember>, ParseError> {
5664        let mut members = Vec::new();
5665        self.consume(TokenType::LBracket)?;
5666        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
5667            let name_tok = self.consume(TokenType::StringLit)?;
5668            let mut member = ExtensionMember {
5669                name: name_tok.value.clone(),
5670                semantics: None,
5671                default_confidence: None,
5672                loc: Loc {
5673                    line: name_tok.line,
5674                    column: name_tok.column,
5675                },
5676            };
5677            // Optional `: { semantics: "…", default_confidence: 0.8 }`.
5678            if self.check(TokenType::Colon) {
5679                self.advance();
5680                self.consume(TokenType::LBrace)?;
5681                while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5682                    let mkey = self.current().value.clone();
5683                    self.advance();
5684                    if self.check(TokenType::Colon) {
5685                        self.advance();
5686                        match mkey.as_str() {
5687                            "semantics" => {
5688                                member.semantics =
5689                                    Some(self.consume(TokenType::StringLit)?.value.clone())
5690                            }
5691                            "default_confidence" => {
5692                                member.default_confidence = self.parse_optional_float()
5693                            }
5694                            _ => self.skip_value(),
5695                        }
5696                    }
5697                    if self.check(TokenType::Comma) {
5698                        self.advance();
5699                    }
5700                }
5701                self.consume(TokenType::RBrace)?;
5702            }
5703            members.push(member);
5704            if self.check(TokenType::Comma) {
5705                self.advance();
5706            }
5707        }
5708        self.consume(TokenType::RBracket)?;
5709        Ok(members)
5710    }
5711
5712    /// §Fase 71.a/e — `window <Name> { timezone: "…"  allow: [ {days hours} ]
5713    /// exclude: [ "YYYY-MM-DD", … ]  on_outside: skip|defer|warn }`.
5714    fn parse_window(&mut self) -> Result<WindowDefinition, ParseError> {
5715        let tok = self.consume(TokenType::Window)?;
5716        let name = self.consume(TokenType::Identifier)?.value;
5717        let mut node = WindowDefinition {
5718            name,
5719            timezone: String::new(),
5720            allow: Vec::new(),
5721            exclude: Vec::new(),
5722            on_outside: String::new(),
5723            loc: Loc {
5724                line: tok.line,
5725                column: tok.column,
5726            },
5727            leading_trivia: Vec::new(),
5728            trailing_trivia: Vec::new(),
5729        };
5730        self.consume(TokenType::LBrace)?;
5731        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5732            let field_name = self.consume_any_ident_or_kw()?.value;
5733            self.consume(TokenType::Colon)?;
5734            match field_name.as_str() {
5735                "timezone" => node.timezone = self.consume(TokenType::StringLit)?.value,
5736                "allow" => node.allow = self.parse_window_allow()?,
5737                "exclude" => node.exclude = self.parse_window_exclude()?,
5738                "on_outside" => node.on_outside = self.consume_any_ident_or_kw()?.value,
5739                _ => self.skip_value(),
5740            }
5741        }
5742        self.consume(TokenType::RBrace)?;
5743        Ok(node)
5744    }
5745
5746    /// §Fase 71.a — the `allow: [ { … }, { … } ]` span list.
5747    fn parse_window_allow(&mut self) -> Result<Vec<WindowSpan>, ParseError> {
5748        self.consume(TokenType::LBracket)?;
5749        let mut spans = Vec::new();
5750        if !self.check(TokenType::RBracket) {
5751            spans.push(self.parse_window_span()?);
5752            while self.check(TokenType::Comma) {
5753                self.advance();
5754                if self.check(TokenType::RBracket) {
5755                    break; // trailing comma
5756                }
5757                spans.push(self.parse_window_span()?);
5758            }
5759        }
5760        self.consume(TokenType::RBracket)?;
5761        Ok(spans)
5762    }
5763
5764    /// §Fase 71.e — the `exclude: [ "YYYY-MM-DD", … ]` holiday list (ISO
5765    /// date-string literals; validated for real-calendar-date-ness by the
5766    /// `axon-T826` type check). An empty list / absent field ⇒ no holidays.
5767    fn parse_window_exclude(&mut self) -> Result<Vec<String>, ParseError> {
5768        self.consume(TokenType::LBracket)?;
5769        let mut dates = Vec::new();
5770        if !self.check(TokenType::RBracket) {
5771            dates.push(self.consume(TokenType::StringLit)?.value);
5772            while self.check(TokenType::Comma) {
5773                self.advance();
5774                if self.check(TokenType::RBracket) {
5775                    break; // trailing comma
5776                }
5777                dates.push(self.consume(TokenType::StringLit)?.value);
5778            }
5779        }
5780        self.consume(TokenType::RBracket)?;
5781        Ok(dates)
5782    }
5783
5784    /// §Fase 71.a — one span `{ days: Mon..Fri  hours: 9..18 }`.
5785    fn parse_window_span(&mut self) -> Result<WindowSpan, ParseError> {
5786        let tok = self.consume(TokenType::LBrace)?;
5787        let mut span = WindowSpan {
5788            day_start: String::new(),
5789            day_end: String::new(),
5790            hour_start: 0,
5791            hour_end: 0,
5792            loc: Loc {
5793                line: tok.line,
5794                column: tok.column,
5795            },
5796        };
5797        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5798            let field = self.consume_any_ident_or_kw()?.value;
5799            self.consume(TokenType::Colon)?;
5800            match field.as_str() {
5801                "days" => {
5802                    span.day_start = self.consume_any_ident_or_kw()?.value;
5803                    self.consume(TokenType::DotDot)?;
5804                    span.day_end = self.consume_any_ident_or_kw()?.value;
5805                }
5806                "hours" => {
5807                    span.hour_start = self.consume_number()? as i64;
5808                    self.consume(TokenType::DotDot)?;
5809                    span.hour_end = self.consume_number()? as i64;
5810                }
5811                _ => self.skip_value(),
5812            }
5813            if self.check(TokenType::Comma) {
5814                self.advance();
5815            }
5816        }
5817        self.consume(TokenType::RBrace)?;
5818        Ok(span)
5819    }
5820
5821    fn parse_shield(&mut self) -> Result<ShieldDefinition, ParseError> {
5822        let tok = self.consume(TokenType::Shield)?;
5823        let name = self.consume(TokenType::Identifier)?.value;
5824        let mut node = ShieldDefinition {
5825            name,
5826            scan: Vec::new(),
5827            strategy: String::new(),
5828            on_breach: String::new(),
5829            severity: String::new(),
5830            quarantine: String::new(),
5831            max_retries: None,
5832            confidence_threshold: None,
5833            allow_tools: Vec::new(),
5834            deny_tools: Vec::new(),
5835            sandbox: None,
5836            redact: Vec::new(),
5837            log: String::new(),
5838            deflect_message: String::new(),
5839            taint: String::new(),
5840            compliance: Vec::new(),
5841            sign: String::new(),
5842            unknown_fields: Vec::new(),
5843            loc: Loc {
5844                line: tok.line,
5845                column: tok.column,
5846            },
5847            leading_trivia: Vec::new(),
5848            trailing_trivia: Vec::new(),
5849        };
5850        self.consume(TokenType::LBrace)?;
5851        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5852            let field_name = self.current().value.clone();
5853            let field_loc = Loc {
5854                line: self.current().line,
5855                column: self.current().column,
5856            };
5857            self.advance();
5858            if self.check(TokenType::Colon) {
5859                self.advance();
5860                match field_name.as_str() {
5861                    "scan" => node.scan = self.parse_bracketed_identifiers()?,
5862                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
5863                    "on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
5864                    "severity" => node.severity = self.consume_any_ident_or_kw()?.value.clone(),
5865                    "quarantine" => {
5866                        node.quarantine = self.consume(TokenType::StringLit)?.value.clone()
5867                    }
5868                    "max_retries" => node.max_retries = self.parse_optional_int(),
5869                    "confidence_threshold" => {
5870                        node.confidence_threshold = self.parse_optional_float()
5871                    }
5872                    "allow_tools" => node.allow_tools = self.parse_bracketed_identifiers()?,
5873                    "deny_tools" => node.deny_tools = self.parse_bracketed_identifiers()?,
5874                    "sandbox" => {
5875                        node.sandbox = Some(self.consume_any_ident_or_kw()?.value == "true")
5876                    }
5877                    "redact" => node.redact = self.parse_bracketed_identifiers()?,
5878                    "log" => node.log = self.consume_any_ident_or_kw()?.value.clone(),
5879                    "deflect_message" => {
5880                        node.deflect_message = self.consume(TokenType::StringLit)?.value.clone()
5881                    }
5882                    "taint" => node.taint = self.consume_any_ident_or_kw()?.value.clone(),
5883                    // ESK Fase 6.1 — covered regulatory classes.
5884                    "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
5885                    // §Fase 77.a — egress signing algorithm (closed catalog,
5886                    // validated by the checker: `axon-T846`).
5887                    "sign" => node.sign = self.consume_any_ident_or_kw()?.value.clone(),
5888                    // §Fase 77.a — the value is still skipped (leniency
5889                    // preserved) but the NAME is recorded so the checker
5890                    // emits `axon-W010` instead of a silent drop.
5891                    _ => {
5892                        node.unknown_fields.push((field_name.clone(), field_loc));
5893                        self.skip_value()
5894                    }
5895                }
5896            } else if self.check(TokenType::LBrace) {
5897                self.skip_braced_block()?;
5898            }
5899        }
5900        self.consume(TokenType::RBrace)?;
5901        Ok(node)
5902    }
5903
5904    fn parse_pix(&mut self) -> Result<PixDefinition, ParseError> {
5905        let tok = self.consume(TokenType::Pix)?;
5906        let name = self.consume(TokenType::Identifier)?.value;
5907        let mut node = PixDefinition {
5908            name,
5909            source: String::new(),
5910            depth: None,
5911            branching: None,
5912            model: String::new(),
5913            loc: Loc {
5914                line: tok.line,
5915                column: tok.column,
5916            },
5917            leading_trivia: Vec::new(),
5918            trailing_trivia: Vec::new(),
5919        };
5920        self.consume(TokenType::LBrace)?;
5921        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5922            let field_name = self.current().value.clone();
5923            self.advance();
5924            if self.check(TokenType::Colon) {
5925                self.advance();
5926                match field_name.as_str() {
5927                    "source" => node.source = self.consume(TokenType::StringLit)?.value.clone(),
5928                    "depth" => node.depth = self.parse_optional_int(),
5929                    "branching" => node.branching = self.parse_optional_int(),
5930                    "model" => node.model = self.consume_any_ident_or_kw()?.value.clone(),
5931                    _ => self.skip_value(),
5932                }
5933            } else if self.check(TokenType::LBrace) {
5934                self.skip_braced_block()?;
5935            }
5936        }
5937        self.consume(TokenType::RBrace)?;
5938        Ok(node)
5939    }
5940
5941    /// §Fase 62.0 — `ledger <Name> { source, depth, branching, model }`.
5942    /// The append-only audit chain (formerly the Provenance-Index reading of
5943    /// `pix`). Field grammar mirrors `pix` (same shape) but the SEMANTICS are
5944    /// audit, not navigation: `depth` = chain retention, `branching` = Merkle
5945    /// factor, `model` = hash slug (sha256 / blake3 / sha3).
5946    fn parse_ledger(&mut self) -> Result<LedgerDefinition, ParseError> {
5947        let tok = self.consume(TokenType::Ledger)?;
5948        let name = self.consume(TokenType::Identifier)?.value;
5949        let mut node = LedgerDefinition {
5950            name,
5951            source: String::new(),
5952            depth: None,
5953            branching: None,
5954            model: String::new(),
5955            loc: Loc {
5956                line: tok.line,
5957                column: tok.column,
5958            },
5959            leading_trivia: Vec::new(),
5960            trailing_trivia: Vec::new(),
5961        };
5962        self.consume(TokenType::LBrace)?;
5963        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5964            let field_name = self.current().value.clone();
5965            self.advance();
5966            if self.check(TokenType::Colon) {
5967                self.advance();
5968                match field_name.as_str() {
5969                    "source" => node.source = self.consume(TokenType::StringLit)?.value.clone(),
5970                    "depth" => node.depth = self.parse_optional_int(),
5971                    "branching" => node.branching = self.parse_optional_int(),
5972                    "model" => node.model = self.consume_any_ident_or_kw()?.value.clone(),
5973                    _ => self.skip_value(),
5974                }
5975            } else if self.check(TokenType::LBrace) {
5976                self.skip_braced_block()?;
5977            }
5978        }
5979        self.consume(TokenType::RBrace)?;
5980        Ok(node)
5981    }
5982
5983    fn parse_psyche(&mut self) -> Result<PsycheDefinition, ParseError> {
5984        let tok = self.consume(TokenType::Psyche)?;
5985        let name = self.consume(TokenType::Identifier)?.value;
5986        let mut node = PsycheDefinition {
5987            name,
5988            dimensions: Vec::new(),
5989            manifold_noise: None,
5990            manifold_momentum: None,
5991            safety_constraints: Vec::new(),
5992            quantum_enabled: None,
5993            inference_mode: String::new(),
5994            loc: Loc {
5995                line: tok.line,
5996                column: tok.column,
5997            },
5998            leading_trivia: Vec::new(),
5999            trailing_trivia: Vec::new(),
6000        };
6001        self.consume(TokenType::LBrace)?;
6002        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6003            let field_name = self.current().value.clone();
6004            self.advance();
6005            if self.check(TokenType::Colon) {
6006                self.advance();
6007                match field_name.as_str() {
6008                    "dimensions" => node.dimensions = self.parse_bracketed_identifiers()?,
6009                    "manifold_noise" => node.manifold_noise = self.parse_optional_float(),
6010                    "manifold_momentum" => node.manifold_momentum = self.parse_optional_float(),
6011                    "safety_constraints" => {
6012                        node.safety_constraints = self.parse_bracketed_identifiers()?
6013                    }
6014                    "quantum_enabled" => {
6015                        node.quantum_enabled = Some(self.consume_any_ident_or_kw()?.value == "true")
6016                    }
6017                    "inference_mode" => {
6018                        node.inference_mode = self.consume_any_ident_or_kw()?.value.clone()
6019                    }
6020                    _ => self.skip_value(),
6021                }
6022            } else if self.check(TokenType::LBrace) {
6023                self.skip_braced_block()?;
6024            }
6025        }
6026        self.consume(TokenType::RBrace)?;
6027        Ok(node)
6028    }
6029
6030    fn parse_corpus(&mut self) -> Result<CorpusDefinition, ParseError> {
6031        let tok = self.consume(TokenType::Corpus)?;
6032        let name = self.consume(TokenType::Identifier)?.value;
6033        let mut node = CorpusDefinition {
6034            name,
6035            documents: Vec::new(),
6036            relations: Vec::new(),
6037            adaptive: false,
6038            mcp_server: String::new(),
6039            mcp_resource_uri: String::new(),
6040            store_source: None,
6041            loc: Loc {
6042                line: tok.line,
6043                column: tok.column,
6044            },
6045            leading_trivia: Vec::new(),
6046            trailing_trivia: Vec::new(),
6047        };
6048        // corpus Name from mcp("server", "uri")  — static MCP-bound short form.
6049        // corpus Name from axonstore { documents: S(id,title)  relations: … }  —
6050        // §Fase 64.A dynamic store-sourced MDN graph (falls through to the body).
6051        let mut dynamic = false;
6052        if self.check(TokenType::From) {
6053            self.advance();
6054            if self.check(TokenType::AxonStore) {
6055                self.advance();
6056                dynamic = true;
6057            } else {
6058                self.consume(TokenType::Mcp)?;
6059                self.consume(TokenType::LParen)?;
6060                node.mcp_server = self.consume(TokenType::StringLit)?.value.clone();
6061                self.consume(TokenType::Comma)?;
6062                node.mcp_resource_uri = self.consume(TokenType::StringLit)?.value.clone();
6063                self.consume(TokenType::RParen)?;
6064                return Ok(node);
6065            }
6066        }
6067        self.consume(TokenType::LBrace)?;
6068        // §Fase 64.A — accumulate the store-mapping pieces while the dynamic body
6069        // is parsed; folded into `node.store_source` after the closing brace.
6070        let mut src = CorpusStoreSource {
6071            doc_store: String::new(),
6072            doc_id_col: String::new(),
6073            doc_title_col: String::new(),
6074            edge_store: String::new(),
6075            edge_from_col: String::new(),
6076            edge_to_col: String::new(),
6077            edge_type_col: String::new(),
6078            edge_weight_col: String::new(),
6079            loc: Loc {
6080                line: tok.line,
6081                column: tok.column,
6082            },
6083        };
6084        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6085            let field_name = self.current().value.clone();
6086            self.advance();
6087            if self.check(TokenType::Colon) {
6088                self.advance();
6089                match field_name.as_str() {
6090                    // §Fase 64.A — dynamic: `documents: <DocStore>(id_col, title_col)`.
6091                    "documents" if dynamic => {
6092                        let (store, cols) = self.parse_corpus_store_mapping(2)?;
6093                        src.doc_store = store;
6094                        src.doc_id_col = cols[0].clone();
6095                        src.doc_title_col = cols[1].clone();
6096                    }
6097                    "documents" => node.documents = self.parse_bracketed_identifiers()?,
6098                    // §Fase 64.A — dynamic: `relations: <EdgeStore>(from, to, etype, weight)`.
6099                    "relations" if dynamic => {
6100                        let (store, cols) = self.parse_corpus_store_mapping(4)?;
6101                        src.edge_store = store;
6102                        src.edge_from_col = cols[0].clone();
6103                        src.edge_to_col = cols[1].clone();
6104                        src.edge_type_col = cols[2].clone();
6105                        src.edge_weight_col = cols[3].clone();
6106                    }
6107                    // §Fase 63.A — static typed weighted edges → MDN corpus graph.
6108                    "relations" => node.relations = self.parse_corpus_relations()?,
6109                    // §Fase 63.C — enable the memory endofunctor.
6110                    "adaptive" => node.adaptive = self.consume_any_ident_or_kw()?.value == "true",
6111                    _ => self.skip_value(),
6112                }
6113            } else if self.check(TokenType::LBrace) {
6114                self.skip_braced_block()?;
6115            }
6116        }
6117        self.consume(TokenType::RBrace)?;
6118        if dynamic {
6119            node.store_source = Some(src);
6120        }
6121        Ok(node)
6122    }
6123
6124    /// §Fase 64.A — parse a store-mapping `<StoreName>( col1, col2, … )` of exactly
6125    /// `n` columns. Used by the dynamic store-sourced corpus's `documents:` (2
6126    /// cols: id, title) and `relations:` (4 cols: from, to, etype, weight). The
6127    /// store name is an identifier (a declared `axonstore`); the columns may be
6128    /// keywords (a column could be named `from`/`type`), so they use the
6129    /// keyword-tolerant consumer. The type-checker validates store + columns.
6130    fn parse_corpus_store_mapping(&mut self, n: usize) -> Result<(String, Vec<String>), ParseError> {
6131        let store = self.consume(TokenType::Identifier)?.value.clone();
6132        self.consume(TokenType::LParen)?;
6133        let mut cols = Vec::with_capacity(n);
6134        for i in 0..n {
6135            if i > 0 {
6136                self.consume(TokenType::Comma)?;
6137            }
6138            cols.push(self.consume_any_ident_or_kw()?.value.clone());
6139        }
6140        self.consume(TokenType::RParen)?;
6141        Ok((store, cols))
6142    }
6143
6144    /// §Fase 63.A — parse `relations: [ etype(from, to, weight) … ]`, the typed
6145    /// weighted edges of an MDN corpus graph. Entries are whitespace/newline
6146    /// separated; commas between them are optional. Edge-type validity (closed
6147    /// catalog), document references, and the weight range are checked by the
6148    /// type-checker (`check_corpus`), not here.
6149    fn parse_corpus_relations(&mut self) -> Result<Vec<CorpusRelation>, ParseError> {
6150        let mut out = Vec::new();
6151        self.consume(TokenType::LBracket)?;
6152        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
6153            if self.check(TokenType::Comma) {
6154                self.advance();
6155                continue;
6156            }
6157            let tok = self.current().clone();
6158            let etype = self.consume_any_ident_or_kw()?.value.clone();
6159            self.consume(TokenType::LParen)?;
6160            let from = self.consume_any_ident_or_kw()?.value.clone();
6161            self.consume(TokenType::Comma)?;
6162            let to = self.consume_any_ident_or_kw()?.value.clone();
6163            self.consume(TokenType::Comma)?;
6164            let weight = self.consume_number()?;
6165            self.consume(TokenType::RParen)?;
6166            out.push(CorpusRelation {
6167                etype,
6168                from,
6169                to,
6170                weight,
6171                loc: Loc { line: tok.line, column: tok.column },
6172            });
6173        }
6174        self.consume(TokenType::RBracket)?;
6175        Ok(out)
6176    }
6177
6178    /// §Fase 108.b — the typed dataspace declaration:
6179    ///
6180    /// ```text
6181    /// dataspace <Name> {
6182    ///     column <name>: <Type>
6183    ///     …
6184    /// }
6185    /// ```
6186    ///
6187    /// Until 108.b the body was consumed by `skip_braced_block()` — any
6188    /// content, including garbage, compiled clean and reached nothing.
6189    /// Now each entry must be a `column` field; the declared type is
6190    /// kept RAW here and resolved against the closed 6-type catalog by
6191    /// the type-checker (`axon-T928`), so all schema errors accumulate
6192    /// in a single compile. An unknown body keyword is a parse error
6193    /// (the grammar is closed — the §38 axonstore posture).
6194    fn parse_dataspace(&mut self) -> Result<DataspaceDefinition, ParseError> {
6195        let tok = self.consume(TokenType::Dataspace)?;
6196        let name = self.consume(TokenType::Identifier)?.value;
6197        let mut node = DataspaceDefinition {
6198            name,
6199            columns: Vec::new(),
6200            loc: Loc {
6201                line: tok.line,
6202                column: tok.column,
6203            },
6204            leading_trivia: Vec::new(),
6205            trailing_trivia: Vec::new(),
6206        };
6207        if self.check(TokenType::LBrace) {
6208            self.consume(TokenType::LBrace)?;
6209            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6210                let entry = self.current().clone();
6211                if entry.value != "column" {
6212                    return Err(ParseError {
6213                        message: format!(
6214                            "Unknown entry `{}` in dataspace `{}`. A dataspace body \
6215                             declares its columnar schema: `column <name>: <Type>` \
6216                             (one per line, over the closed type catalog — \
6217                             Text, Int, Float, Bool, Timestamp, Json).",
6218                            entry.value, node.name
6219                        ),
6220                        line: entry.line,
6221                        column: entry.column,
6222                        ..Default::default()
6223                    });
6224                }
6225                self.advance(); // `column`
6226                let col_tok = self.current().clone();
6227                let col_name = self.consume_any_ident_or_kw()?.value.clone();
6228                self.consume(TokenType::Colon)?;
6229                let declared_type = self.consume_any_ident_or_kw()?.value.clone();
6230                node.columns.push(crate::ast::DataspaceColumn {
6231                    name: col_name,
6232                    declared_type,
6233                    loc: Loc {
6234                        line: col_tok.line,
6235                        column: col_tok.column,
6236                    },
6237                });
6238            }
6239            self.consume(TokenType::RBrace)?;
6240        }
6241        Ok(node)
6242    }
6243
6244    fn parse_ots(&mut self) -> Result<OtsDefinition, ParseError> {
6245        let tok = self.consume(TokenType::Ots)?;
6246        let name = self.consume(TokenType::Identifier)?.value;
6247        let mut node = OtsDefinition {
6248            name,
6249            teleology: String::new(),
6250            homotopy_search: String::new(),
6251            loss_function: String::new(),
6252            loc: Loc {
6253                line: tok.line,
6254                column: tok.column,
6255            },
6256            leading_trivia: Vec::new(),
6257            trailing_trivia: Vec::new(),
6258        };
6259        // Skip optional type params <In, Out>
6260        if self.check(TokenType::Lt) {
6261            while !self.check(TokenType::Gt) && !self.check(TokenType::Eof) {
6262                self.advance();
6263            }
6264            if self.check(TokenType::Gt) {
6265                self.advance();
6266            }
6267        }
6268        self.consume(TokenType::LBrace)?;
6269        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6270            let field_name = self.current().value.clone();
6271            self.advance();
6272            if self.check(TokenType::Colon) {
6273                self.advance();
6274                match field_name.as_str() {
6275                    "teleology" => {
6276                        node.teleology = self.consume(TokenType::StringLit)?.value.clone()
6277                    }
6278                    "homotopy_search" => {
6279                        node.homotopy_search = self.consume_any_ident_or_kw()?.value.clone()
6280                    }
6281                    "loss_function" => {
6282                        node.loss_function = self.consume(TokenType::StringLit)?.value.clone()
6283                    }
6284                    _ => self.skip_value(),
6285                }
6286            } else if self.check(TokenType::LBrace) {
6287                self.skip_braced_block()?;
6288            }
6289        }
6290        self.consume(TokenType::RBrace)?;
6291        Ok(node)
6292    }
6293
6294    fn parse_mandate(&mut self) -> Result<MandateDefinition, ParseError> {
6295        let tok = self.consume(TokenType::Mandate)?;
6296        let name = self.consume(TokenType::Identifier)?.value;
6297        let mut node = MandateDefinition {
6298            name,
6299            constraint: String::new(),
6300            kp: None,
6301            ki: None,
6302            kd: None,
6303            tolerance: None,
6304            max_steps: None,
6305            on_violation: String::new(),
6306            loc: Loc {
6307                line: tok.line,
6308                column: tok.column,
6309            },
6310            leading_trivia: Vec::new(),
6311            trailing_trivia: Vec::new(),
6312        };
6313        self.consume(TokenType::LBrace)?;
6314        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6315            let field_name = self.current().value.clone();
6316            self.advance();
6317            if self.check(TokenType::Colon) {
6318                self.advance();
6319                match field_name.as_str() {
6320                    "constraint" => {
6321                        node.constraint = self.consume(TokenType::StringLit)?.value.clone()
6322                    }
6323                    "kp" | "Kp" => node.kp = self.parse_optional_float(),
6324                    "ki" | "Ki" => node.ki = self.parse_optional_float(),
6325                    "kd" | "Kd" => node.kd = self.parse_optional_float(),
6326                    "tolerance" => node.tolerance = self.parse_optional_float(),
6327                    "max_steps" => node.max_steps = self.parse_optional_int(),
6328                    "on_violation" => {
6329                        node.on_violation = self.consume_any_ident_or_kw()?.value.clone()
6330                    }
6331                    _ => self.skip_value(),
6332                }
6333            } else if self.check(TokenType::LBrace) {
6334                self.skip_braced_block()?;
6335            }
6336        }
6337        self.consume(TokenType::RBrace)?;
6338        Ok(node)
6339    }
6340
6341    /// §Fase 111.f — `compute <Name>(p: T, …) -> T { <expr> }`.
6342    ///
6343    /// # What this used to be
6344    ///
6345    /// ```text
6346    /// // Skip optional parameters/return type before brace
6347    /// while !self.check(TokenType::LBrace) { self.advance(); }
6348    /// ```
6349    ///
6350    /// The parameters and the return type were **skipped token by token**, and
6351    /// the brace held only `shield:`. So a `compute` had **no inputs, no output
6352    /// type and no body** — which is why the runtime could do nothing but bind
6353    /// the literal string `"compute:Name(args)"`, and why a downstream step then
6354    /// consumed that text where it expected a number. The README meanwhile
6355    /// promised "native Fast-Path execution bypassing the LLM" **with an O(n)
6356    /// guarantee**.
6357    ///
6358    /// # What it is now
6359    ///
6360    /// A named pure function over the §70 expression language — the closed,
6361    /// total, side-effect-free term algebra the runtime already evaluates
6362    /// natively (`eval_expr`, the same evaluator behind `let`, `grad` and
6363    /// `conditional`). Linear in the term, no model in the loop: the advertised
6364    /// claim, made true rather than louder.
6365    ///
6366    /// The legacy field form (`compute N { shield: G }`) still parses — its body
6367    /// is simply `None`, and applying a bodyless compute is refused (axon-T941)
6368    /// instead of silently binding a placeholder.
6369    fn parse_compute(&mut self) -> Result<ComputeDefinition, ParseError> {
6370        let tok = self.consume(TokenType::Compute)?;
6371        let name = self.consume(TokenType::Identifier)?.value;
6372        let mut node = ComputeDefinition {
6373            name,
6374            shield_ref: String::new(),
6375            parameters: Vec::new(),
6376            return_type: String::new(),
6377            body: None,
6378            loc: Loc {
6379                line: tok.line,
6380                column: tok.column,
6381            },
6382            leading_trivia: Vec::new(),
6383            trailing_trivia: Vec::new(),
6384        };
6385
6386        // `(p: T, q: T)` — the typed parameters (they used to be skipped).
6387        if self.check(TokenType::LParen) {
6388            self.advance();
6389            while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
6390                let ptok = self.current().clone();
6391                let pname = self.consume_any_ident_or_kw()?.value.clone();
6392                self.consume(TokenType::Colon)?;
6393                let ptype = self.parse_type_expr()?;
6394                node.parameters.push(Parameter {
6395                    name: pname,
6396                    type_expr: ptype,
6397                    loc: self.loc_of(&ptok),
6398                });
6399                if self.check(TokenType::Comma) {
6400                    self.advance();
6401                }
6402            }
6403            self.consume(TokenType::RParen)?;
6404        }
6405
6406        // `-> T` — the declared result type.
6407        if self.check(TokenType::Arrow) {
6408            self.advance();
6409            node.return_type = self.consume_any_ident_or_kw()?.value.clone();
6410        }
6411
6412        self.consume(TokenType::LBrace)?;
6413        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6414            // A `<name>:` pair is a legacy field (only `shield:` is meaningful).
6415            // Anything else is THE BODY — a §70 expression.
6416            //
6417            // NOTE: the field name may be a KEYWORD, not just an identifier —
6418            // `shield` is `TokenType::Shield`. Testing only for `Identifier` here
6419            // sent `compute N { shield: G }` (the legacy declaration form, and
6420            // the shape of the shipped canonical program) down the
6421            // expression-parsing path and broke it. Back-compat is not optional:
6422            // an adopter's existing program must keep compiling.
6423            let is_field = self
6424                .tokens
6425                .get(self.pos + 1)
6426                .map(|t| t.ttype == TokenType::Colon)
6427                .unwrap_or(false);
6428            if is_field {
6429                let field_name = self.current().value.clone();
6430                self.advance();
6431                self.consume(TokenType::Colon)?;
6432                match field_name.as_str() {
6433                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
6434                    _ => self.skip_value(),
6435                }
6436            } else {
6437                node.body = Some(self.parse_expr()?);
6438            }
6439        }
6440        self.consume(TokenType::RBrace)?;
6441        Ok(node)
6442    }
6443
6444    fn parse_daemon(&mut self) -> Result<DaemonDefinition, ParseError> {
6445        let tok = self.consume(TokenType::Daemon)?;
6446        let name = self.consume(TokenType::Identifier)?.value;
6447        let mut node = DaemonDefinition {
6448            name,
6449            goal: String::new(),
6450            tools: Vec::new(),
6451            memory_ref: String::new(),
6452            strategy: String::new(),
6453            on_stuck: String::new(),
6454            shield_ref: String::new(),
6455            window_ref: String::new(),
6456            budget: None,
6457            max_tokens: None,
6458            max_time: String::new(),
6459            max_cost: None,
6460            listeners: Vec::new(),
6461            requires_capabilities: Vec::new(),
6462            loc: Loc {
6463                line: tok.line,
6464                column: tok.column,
6465            },
6466            leading_trivia: Vec::new(),
6467            trailing_trivia: Vec::new(),
6468        };
6469        // Skip optional parameters/return type before brace
6470        while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
6471            self.advance();
6472        }
6473        self.consume(TokenType::LBrace)?;
6474        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6475            let field = self.current().clone();
6476            let field_name = field.value.clone();
6477            self.advance();
6478            if self.check(TokenType::Colon) {
6479                self.advance();
6480                match field_name.as_str() {
6481                    "goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
6482                    "tools" => node.tools = self.parse_bracketed_identifiers()?,
6483                    "memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
6484                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
6485                    "on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
6486                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
6487                    // §Fase 71.c — `window: <WindowName>` temporal binding.
6488                    "window" => node.window_ref = self.consume_any_ident_or_kw()?.value.clone(),
6489                    "max_tokens" => node.max_tokens = self.parse_optional_int(),
6490                    "max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
6491                    "max_cost" => node.max_cost = self.parse_optional_float(),
6492                    // §Fase 52.d — `requires: [cap, …]` capability scope (same
6493                    // closed slug grammar as `axonendpoint requires:`). The
6494                    // enterprise supervisor mints a per-run principal scoped to
6495                    // exactly these (least privilege).
6496                    "requires" => {
6497                        let bracket_tok = self.current().clone();
6498                        let items = self.parse_bracketed_dot_identifiers()?;
6499                        for slug in &items {
6500                            if !is_valid_capability_slug(slug) {
6501                                return Err(ParseError {
6502                                    message: format!(
6503                                        "Invalid capability slug '{slug}' in daemon '{}' \
6504                                         `requires:`. Capability slugs must match \
6505                                         ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
6506                                         lowercase identifiers. Examples: `daemon.run`, \
6507                                         `memory.write`, `flow.execute`.",
6508                                        node.name
6509                                    ),
6510                                    line: bracket_tok.line,
6511                                    column: bracket_tok.column,
6512                                    ..Default::default()
6513                                });
6514                            }
6515                        }
6516                        node.requires_capabilities = items;
6517                    }
6518                    _ => self.skip_value(),
6519                }
6520            } else if field.ttype == TokenType::Listen {
6521                // §λ-L-E Fase 13 D4 — preserve listen blocks for type
6522                // checking.  We backtracked past the `listen` keyword
6523                // by `advance()` above, so reconstruct a synthetic
6524                // listener using the same dual-mode dispatch the flow
6525                // step parser uses (string topic OR typed channel ref).
6526                let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
6527                    (self.consume(TokenType::StringLit)?.value.clone(), false)
6528                } else {
6529                    (self.consume_any_ident_or_kw()?.value.clone(), true)
6530                };
6531                let mut alias = String::new();
6532                if !self.at_declaration_start()
6533                    && !self.check(TokenType::RBrace)
6534                    && !self.check(TokenType::LBrace)
6535                {
6536                    let next = self.current().clone();
6537                    if next.value == "as" || next.ttype == TokenType::As {
6538                        self.advance();
6539                        alias = self.consume_any_ident_or_kw()?.value.clone();
6540                    }
6541                }
6542                let listen_loc = Loc {
6543                    line: field.line,
6544                    column: field.column,
6545                };
6546                // §Fase 52.a — parse the handler body (was skipped). This is
6547                // what makes a `daemon` operational: the body runs per event /
6548                // scheduled tick (e.g. a `listen "cron:…" as tick { run … }`).
6549                let body = self.parse_listener_body()?;
6550                node.listeners.push(ListenStep {
6551                    channel,
6552                    channel_is_ref,
6553                    event_alias: alias,
6554                    body,
6555                    loc: listen_loc,
6556                });
6557            } else if field_name == "budget" && self.check(TokenType::LBrace) {
6558                // §Fase 72.a — the `budget { … }` linear-effect rate-limit block.
6559                node.budget = Some(self.parse_budget_block(field.line, field.column)?);
6560            } else if self.check(TokenType::LBrace) {
6561                self.skip_braced_block()?;
6562            }
6563        }
6564        self.consume(TokenType::RBrace)?;
6565        Ok(node)
6566    }
6567
6568    /// §Fase 114.a — a TOP-LEVEL `budget <Name> { … }`.
6569    ///
6570    /// Same body as the daemon-attached block; what it gains is a **name** and a
6571    /// **scope that is not a daemon**. Until §114, `budget` was a field of `daemon`
6572    /// and of nothing else — so an adopter deploying an HTTP endpoint that calls a
6573    /// vendor tool had **no way in the language to bound how often it did that.**
6574    /// Not "the bound did not work": **the bound could not be written.** And the
6575    /// HTTP endpoint is what people actually deploy.
6576    fn parse_top_level_budget(&mut self) -> Result<BudgetBlock, ParseError> {
6577        let kw = self.consume(TokenType::Budget)?; // `budget`
6578        let name = self.consume(TokenType::Identifier)?.value;
6579        let mut block = self.parse_budget_block(kw.line, kw.column)?;
6580        block.name = name;
6581        Ok(block)
6582    }
6583
6584    /// §Fase 72.a — `budget { <rate|max>: N per <period> on Tool(<X>) … [on_exhausted: <p>] }`.
6585    fn parse_budget_block(&mut self, line: u32, column: u32) -> Result<BudgetBlock, ParseError> {
6586        self.consume(TokenType::LBrace)?;
6587        let mut quotas = Vec::new();
6588        let mut on_exhausted = String::new();
6589        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6590            let field = self.current().clone();
6591            let field_name = self.consume_any_ident_or_kw()?.value;
6592            match field_name.as_str() {
6593                "rate" | "max" => {
6594                    quotas.push(self.parse_budget_quota(field_name, field.line, field.column)?);
6595                }
6596                "on_exhausted" => {
6597                    self.consume(TokenType::Colon)?;
6598                    on_exhausted = self.consume_any_ident_or_kw()?.value;
6599                }
6600                _ => self.skip_value(),
6601            }
6602        }
6603        self.consume(TokenType::RBrace)?;
6604        Ok(BudgetBlock {
6605            name: String::new(),
6606            quotas,
6607            on_exhausted,
6608            loc: Loc { line, column },
6609            leading_trivia: Vec::new(),
6610            trailing_trivia: Vec::new(),
6611        })
6612    }
6613
6614    /// §Fase 72.a — one quota line: `<kind>: <limit> per <period> on Tool(<effect>)`.
6615    /// `kind` (`rate`/`max`) is already consumed by the caller.
6616    fn parse_budget_quota(
6617        &mut self,
6618        kind: String,
6619        line: u32,
6620        column: u32,
6621    ) -> Result<BudgetQuota, ParseError> {
6622        self.consume(TokenType::Colon)?;
6623        let limit = self.consume_number()? as i64;
6624        // `per <period>`
6625        let _per = self.consume_any_ident_or_kw()?; // the `per` keyword
6626        let period = self.consume_any_ident_or_kw()?.value;
6627        // `on Tool(<effect>)`
6628        let _on = self.consume_any_ident_or_kw()?; // the `on` keyword
6629        let _tool = self.consume_any_ident_or_kw()?; // the `Tool` wrapper keyword
6630        self.consume(TokenType::LParen)?;
6631        let effect = self.consume_any_ident_or_kw()?.value;
6632        self.consume(TokenType::RParen)?;
6633        Ok(BudgetQuota {
6634            kind,
6635            limit,
6636            period,
6637            effect,
6638            loc: Loc { line, column },
6639        })
6640    }
6641
6642    fn parse_axonstore(&mut self) -> Result<AxonStoreDefinition, ParseError> {
6643        let tok = self.consume(TokenType::AxonStore)?;
6644        let name = self.consume(TokenType::Identifier)?.value;
6645        let mut node = AxonStoreDefinition {
6646            name,
6647            backend: String::new(),
6648            connection: String::new(),
6649            resource_ref: String::new(),
6650            confidence_floor: None,
6651            isolation: String::new(),
6652            on_breach: String::new(),
6653            capability: String::new(),
6654            class: String::new(),
6655            column_schema: None,
6656            loc: Loc {
6657                line: tok.line,
6658                column: tok.column,
6659            },
6660            leading_trivia: Vec::new(),
6661            trailing_trivia: Vec::new(),
6662        };
6663        self.consume(TokenType::LBrace)?;
6664        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6665            let field = self.current().clone();
6666            let field_name = field.value.clone();
6667            // §Fase 38.b (D1) — `schema:` declaration in three closed
6668            // forms: inline column block, manifest reference (string
6669            // literal), or env-var schema namespace (`env:VAR` —
6670            // unquoted or quoted). Parse the form; the §38.d / §38.e
6671            // type-checker consumes the resulting AST.
6672            if field.ttype == TokenType::Schema {
6673                self.advance();
6674                let parsed = self.parse_store_schema_declaration(&node.name, field.line, field.column)?;
6675                node.column_schema = Some(parsed);
6676                continue;
6677            }
6678            self.advance();
6679            if self.check(TokenType::Colon) {
6680                self.advance();
6681                match field_name.as_str() {
6682                    "backend" => node.backend = self.consume_any_ident_or_kw()?.value.clone(),
6683                    // §Fase 94.a — the secret-class prefix of a
6684                    // `backend: secrets` metadata store. Dotted-identifier
6685                    // form (`class: crm`, `class: crm.oauth`); the
6686                    // secrets-only placement rule + slug shape are
6687                    // `axon-T900` in the type-checker (it needs the
6688                    // resolved `backend:`, which may appear after this
6689                    // field in source order).
6690                    "class" => node.class = self.parse_dotted_identifier()?,
6691                    "connection" => {
6692                        node.connection = self.consume(TokenType::StringLit)?.value.clone()
6693                    }
6694                    // §Fase 113 — the `resource` this store RUNS ON. When
6695                    // present the store derives its DSN, its POOL SIZE and its
6696                    // sharing discipline from the resource; `connection:`
6697                    // becomes redundant and `axon-T946` refuses declaring both
6698                    // (the same fact, twice, is how the islands happened).
6699                    "resource" => {
6700                        node.resource_ref = self.consume_any_ident_or_kw()?.value.clone()
6701                    }
6702                    "confidence_floor" => node.confidence_floor = self.parse_optional_float(),
6703                    "isolation" => node.isolation = self.consume_any_ident_or_kw()?.value.clone(),
6704                    "on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
6705                    // §Fase 35.j (D11) — Pillar IV: the capability slug
6706                    // required to access this store. Validated against
6707                    // the closed slug grammar shared with `requires:`.
6708                    "capability" => {
6709                        let slug_tok = self.consume(TokenType::StringLit)?.clone();
6710                        if !is_valid_capability_slug(&slug_tok.value) {
6711                            return Err(ParseError {
6712                                message: format!(
6713                                    "Invalid capability slug '{}' in axonstore '{}' \
6714                                     `capability:`. Capability slugs must match \
6715                                     ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
6716                                     lowercase identifiers starting with a letter. Examples: \
6717                                     `admin`, `tenant.read`, `hipaa.phi.read`.",
6718                                    slug_tok.value, node.name
6719                                ),
6720                                line: slug_tok.line,
6721                                column: slug_tok.column,
6722                                ..Default::default()
6723                            });
6724                        }
6725                        node.capability = slug_tok.value.clone();
6726                    }
6727                    _ => self.skip_value(),
6728                }
6729            } else if self.check(TokenType::LBrace) {
6730                self.skip_braced_block()?;
6731            }
6732        }
6733        self.consume(TokenType::RBrace)?;
6734        Ok(node)
6735    }
6736
6737    /// §Fase 38.b (D1) — parse the three closed forms of an `axonstore`
6738    /// `schema:` declaration:
6739    ///
6740    ///   * form (a) **inline** — `schema { col: Type [constraint…], … }`
6741    ///   * form (b) **manifest reference** — `schema: "qualified.name"`
6742    ///     (string literal that does NOT start with `env:`)
6743    ///   * form (c) **env-var schema namespace** — `schema: env:VAR`
6744    ///     (unquoted) OR `schema: "env:VAR"` (quoted; the literal
6745    ///     starts with `env:`)
6746    ///
6747    /// Called immediately AFTER `schema` is consumed.
6748    fn parse_store_schema_declaration(
6749        &mut self,
6750        store_name: &str,
6751        sch_line: u32,
6752        sch_col: u32,
6753    ) -> Result<crate::store_schema::StoreColumnSchema, ParseError> {
6754        use crate::store_schema::{StoreColumn, StoreColumnSchema, StoreColumnType};
6755
6756        // — Form (a) — inline column block: `schema { ... }`. —
6757        if self.check(TokenType::LBrace) {
6758            self.consume(TokenType::LBrace)?;
6759            let mut columns: Vec<StoreColumn> = Vec::new();
6760            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6761                let col_tok = self.current().clone();
6762                let col_name = self.consume_any_ident_or_kw()?.value.clone();
6763                self.consume(TokenType::Colon)?;
6764                let type_tok = self.consume_any_ident_or_kw()?.clone();
6765                let col_type = StoreColumnType::from_token(&type_tok.value).ok_or_else(|| {
6766                    let names = StoreColumnType::all_canonical_names();
6767                    let suggestion =
6768                        crate::smart_suggest::suggest_for(&type_tok.value, &names);
6769                    let suggest_suffix = if suggestion.is_empty() {
6770                        String::new()
6771                    } else {
6772                        format!(" {suggestion}")
6773                    };
6774                    let known = names.join(", ");
6775                    ParseError {
6776                        message: format!(
6777                            "Unknown column type `{}` for column `{}` in \
6778                             axonstore `{}` `schema:` block. The closed \
6779                             v1.38.0 column-type catalog (Fase 38.b D1) \
6780                             is {{{known}}} (plus common lowercase \
6781                             aliases — `int`/`integer`/`int4` for \
6782                             `Int`, `bool`/`boolean` for `Bool`, etc.).\
6783                             {suggest_suffix}",
6784                            type_tok.value, col_name, store_name
6785                        ),
6786                        line: type_tok.line,
6787                        column: type_tok.column,
6788                        ..Default::default()
6789                    }
6790                })?;
6791
6792                // §Fase 73.a (D1) — the OPTIONAL `Json<T>` shape LENS on a
6793                // column. `payload: Json<UserEvent>` records the expected
6794                // struct shape; the lens is a compile-time expectation only
6795                // (the column stays physically `jsonb`, navigated totally at
6796                // runtime — doctrine `open_data_is_total`). The shape's
6797                // well-formedness (T is a declared `type`) is `axon-T840`
6798                // in the type-checker — it needs the symbol table. Here we
6799                // only enforce the STRUCTURAL rule: a `<T>` lens may refine
6800                // ONLY a `Json` / `Jsonb` column — `axon-T841` otherwise.
6801                let mut json_shape: Option<String> = None;
6802                if self.check(TokenType::Lt) {
6803                    self.advance();
6804                    let shape_tok = self.consume_any_ident_or_kw()?.clone();
6805                    self.consume(TokenType::Gt)?;
6806                    if matches!(col_type, StoreColumnType::Json | StoreColumnType::Jsonb) {
6807                        json_shape = Some(shape_tok.value.clone());
6808                    } else {
6809                        return Err(ParseError {
6810                            message: format!(
6811                                "axon-T841 a shape lens `<{shape}>` may refine \
6812                                 only a `Json` / `Jsonb` column, but column \
6813                                 `{col}` in axonstore `{store}` is `{ty}`. Drop \
6814                                 the `<{shape}>` (a rigid column already has a \
6815                                 fixed shape), or change the column type to \
6816                                 `Json<{shape}>` if it carries open documents.",
6817                                shape = shape_tok.value,
6818                                col = col_name,
6819                                store = store_name,
6820                                ty = col_type.canonical_name(),
6821                            ),
6822                            line: shape_tok.line,
6823                            column: shape_tok.column,
6824                            ..Default::default()
6825                        });
6826                    }
6827                }
6828
6829                let mut col = StoreColumn {
6830                    name: col_name,
6831                    col_type,
6832                    json_shape,
6833                    primary_key: false,
6834                    auto_increment: false,
6835                    not_null: false,
6836                    unique: false,
6837                    indexed: false,
6838                    default_value: String::new(),
6839                    // §Fase 38.x.d (D1) — `identity` is now a recognized
6840                    // inline keyword (see the constraint loop below).
6841                    // Defaults to false; set to true when the adopter
6842                    // writes `id: BigInt primary_key identity`.
6843                    identity: false,
6844                    line: col_tok.line,
6845                    column: col_tok.column,
6846                };
6847
6848                // Trailing constraints (position-independent), matching
6849                // the Python `_parse_store_column` surface.
6850                while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6851                    if self.current().ttype != TokenType::Identifier {
6852                        // The next column starts with a non-identifier
6853                        // (rare) — stop the constraint scan.
6854                        break;
6855                    }
6856                    let constraint = self.current().value.clone();
6857                    match constraint.as_str() {
6858                        "primary_key" => {
6859                            col.primary_key = true;
6860                            self.advance();
6861                        }
6862                        "auto_increment" => {
6863                            col.auto_increment = true;
6864                            self.advance();
6865                        }
6866                        "not_null" => {
6867                            col.not_null = true;
6868                            self.advance();
6869                        }
6870                        "unique" => {
6871                            col.unique = true;
6872                            self.advance();
6873                        }
6874                        // §Fase 73.f (D1) — the `index` constraint declares
6875                        // an index as a capability-honest effect (visible to
6876                        // the deploy gate, not a silent DBA action). The
6877                        // backend picks the method from the column type
6878                        // (GIN for a Json/Jsonb column, b-tree otherwise).
6879                        "index" => {
6880                            col.indexed = true;
6881                            self.advance();
6882                        }
6883                        // §Fase 38.x.d (D1) — `identity` marks a column
6884                        // as `GENERATED ALWAYS/BY DEFAULT AS IDENTITY`.
6885                        // Distinct from `auto_increment` (legacy SERIAL
6886                        // via `nextval(...)` default). T803 skips
6887                        // identity columns from the NOT-NULL-omission
6888                        // check because Postgres auto-fills them; the
6889                        // distinction matters because IDENTITY ALWAYS
6890                        // also rejects user-supplied values, where
6891                        // SERIAL accepts them (a future 38.x.e arm in
6892                        // T802 may surface this).
6893                        "identity" => {
6894                            col.identity = true;
6895                            self.advance();
6896                        }
6897                        "default" => {
6898                            self.advance();
6899                            let dv = self.current().clone();
6900                            if matches!(
6901                                dv.ttype,
6902                                TokenType::StringLit
6903                                    | TokenType::Integer
6904                                    | TokenType::Float
6905                            ) {
6906                                col.default_value = dv.value.clone();
6907                                self.advance();
6908                            } else {
6909                                col.default_value =
6910                                    self.consume_any_ident_or_kw()?.value.clone();
6911                            }
6912                        }
6913                        _ => break,
6914                    }
6915                }
6916
6917                columns.push(col);
6918            }
6919            self.consume(TokenType::RBrace)?;
6920            return Ok(StoreColumnSchema::Inline {
6921                columns,
6922                leading_trivia: Vec::new(),
6923                line: sch_line,
6924                column: sch_col,
6925            });
6926        }
6927
6928        // — Forms (b) + (c) require a `:` separator. —
6929        if !self.check(TokenType::Colon) {
6930            let cur = self.current().clone();
6931            return Err(ParseError {
6932                message: format!(
6933                    "axonstore `{store_name}` `schema:` declaration expects \
6934                     `{{ … }}` (inline columns), `: \"manifest.ref\"` \
6935                     (manifest reference), or `: env:VAR` (per-tenant schema \
6936                     namespace). Got `{}` instead.",
6937                    cur.value
6938                ),
6939                line: cur.line,
6940                column: cur.column,
6941                ..Default::default()
6942            });
6943        }
6944        self.consume(TokenType::Colon)?;
6945
6946        // — Form (b) or (c)-quoted — string literal value. —
6947        if self.check(TokenType::StringLit) {
6948            let lit = self.consume(TokenType::StringLit)?.clone();
6949            let value = lit.value.clone();
6950            if let Some(var) = value.strip_prefix("env:") {
6951                let var = var.trim();
6952                if var.is_empty() {
6953                    return Err(ParseError {
6954                        message: format!(
6955                            "axonstore `{store_name}` `schema: \"env:\"` is \
6956                             missing the variable name after the `env:` \
6957                             prefix."
6958                        ),
6959                        line: lit.line,
6960                        column: lit.column,
6961                        ..Default::default()
6962                    });
6963                }
6964                return Ok(StoreColumnSchema::EnvVar {
6965                    var_name: var.to_string(),
6966                    line: sch_line,
6967                    column: sch_col,
6968                });
6969            }
6970            // Plain string → manifest reference.
6971            if value.trim().is_empty() {
6972                return Err(ParseError {
6973                    message: format!(
6974                        "axonstore `{store_name}` `schema:` manifest reference \
6975                         is empty. Expected `\"qualified.name\"` — e.g. \
6976                         `\"public.tenants\"`."
6977                    ),
6978                    line: lit.line,
6979                    column: lit.column,
6980                    ..Default::default()
6981                });
6982            }
6983            return Ok(StoreColumnSchema::ManifestRef {
6984                qualified_name: value,
6985                line: sch_line,
6986                column: sch_col,
6987            });
6988        }
6989
6990        // — Form (c) unquoted — `env:VAR`. The lexer emits `env` as an
6991        //   identifier, then `:`, then the identifier var name. —
6992        let env_tok = self.current().clone();
6993        if env_tok.value == "env" {
6994            self.advance();
6995            if !self.check(TokenType::Colon) {
6996                return Err(ParseError {
6997                    message: format!(
6998                        "axonstore `{store_name}` `schema: env` is missing the \
6999                         `:` separator. Expected `schema: env:VAR`."
7000                    ),
7001                    line: env_tok.line,
7002                    column: env_tok.column,
7003                    ..Default::default()
7004                });
7005            }
7006            self.advance(); // past ':'
7007            let var_tok = self.consume_any_ident_or_kw()?.clone();
7008            if var_tok.value.trim().is_empty() {
7009                return Err(ParseError {
7010                    message: format!(
7011                        "axonstore `{store_name}` `schema: env:` is missing \
7012                         the variable name."
7013                    ),
7014                    line: var_tok.line,
7015                    column: var_tok.column,
7016                    ..Default::default()
7017                });
7018            }
7019            return Ok(StoreColumnSchema::EnvVar {
7020                var_name: var_tok.value.clone(),
7021                line: sch_line,
7022                column: sch_col,
7023            });
7024        }
7025
7026        Err(ParseError {
7027            message: format!(
7028                "axonstore `{store_name}` `schema:` declaration expects \
7029                 `{{ … }}` (inline columns), `\"manifest.ref\"` (manifest \
7030                 reference), or `env:VAR` (per-tenant schema namespace). \
7031                 Got `{}` instead.",
7032                env_tok.value
7033            ),
7034            line: env_tok.line,
7035            column: env_tok.column,
7036            ..Default::default()
7037        })
7038    }
7039
7040    // ── §λ-L-E Fase 1 — Resource primitive ────────────────────────
7041
7042    /// Parse: `resource Name { kind, endpoint, capacity, lifetime, certainty_floor, shield }`.
7043    ///
7044    /// Mirrors `axon.compiler.parser.Parser._parse_resource`. Unknown fields
7045    /// are silently skipped (keeps the grammar forward-compatible).
7046    fn parse_resource(&mut self) -> Result<ResourceDefinition, ParseError> {
7047        let tok = self.consume(TokenType::Resource)?;
7048        let name = self.consume(TokenType::Identifier)?.value;
7049        let mut node = ResourceDefinition {
7050            name,
7051            kind: String::new(),
7052            endpoint: String::new(),
7053            capacity: None,
7054            lifetime: "affine".to_string(),
7055            certainty_floor: None,
7056            shield_ref: String::new(),
7057            within: String::new(),
7058            loc: Loc {
7059                line: tok.line,
7060                column: tok.column,
7061            },
7062            leading_trivia: Vec::new(),
7063            trailing_trivia: Vec::new(),
7064        };
7065        self.consume(TokenType::LBrace)?;
7066        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7067            let field_tok = self.current().clone();
7068            let field_name = field_tok.value.clone();
7069            self.advance();
7070            if !self.check(TokenType::Colon) {
7071                // Tolerate stray brace or unknown layout.
7072                if self.check(TokenType::LBrace) {
7073                    self.skip_braced_block()?;
7074                }
7075                continue;
7076            }
7077            self.advance(); // past ':'
7078            match field_name.as_str() {
7079                "kind" => node.kind = self.consume_any_ident_or_kw()?.value,
7080                // §Fase 113 — `endpoint:` accepts BOTH shapes on purpose:
7081                //   - a dotted config key  (`endpoint: db.main`)      — the law
7082                //   - a string literal     (`endpoint: "postgres://…"`) — the sin
7083                //
7084                // The literal is REFUSED, but by `axon-T944`, not by the parser.
7085                // If it died here the adopter would read "Expected StringLit",
7086                // which explains nothing. The law gets to say why: *URLs and
7087                // credentials never appear in source* — the same sentence
7088                // `axon-T850` has been saying to `upstream.resolve` all along.
7089                //
7090                // A diagnostic that names the rule teaches; one that names the
7091                // token type only tells you the compiler is unhappy.
7092                "endpoint" => {
7093                    node.endpoint = if self.check(TokenType::StringLit) {
7094                        self.consume(TokenType::StringLit)?.value
7095                    } else {
7096                        self.parse_dotted_identifier()?
7097                    };
7098                }
7099                "capacity" => {
7100                    node.capacity = self.parse_optional_int();
7101                }
7102                "lifetime" => {
7103                    let lt_tok = self.consume_any_ident_or_kw()?;
7104                    let lt = lt_tok.value;
7105                    if !matches!(lt.as_str(), "linear" | "affine" | "persistent") {
7106                        return Err(ParseError {
7107                            message: format!(
7108                                "Invalid lifetime '{lt}' in resource '{}' — \
7109                                 expected linear | affine | persistent",
7110                                node.name
7111                            ),
7112                            line: lt_tok.line,
7113                            column: lt_tok.column,
7114                                                    ..Default::default()
7115                        });
7116                    }
7117                    node.lifetime = lt;
7118                }
7119                "certainty_floor" => {
7120                    node.certainty_floor = self.parse_optional_float();
7121                }
7122                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
7123                // §Fase 113 — `within: <fabric>`. ONE field, so a resource
7124                // cannot be in two fabrics: Separation-Logic disjointness is
7125                // unrepresentable rather than verified.
7126                "within" => node.within = self.consume_any_ident_or_kw()?.value,
7127                // §Fase 113 — an unknown field is a HARD ERROR, not a shrug.
7128                //
7129                // This arm used to be `_ => self.skip_value()`. That is the same
7130                // family as §111's root cause (`parse_block_step` →
7131                // `skip_braced_block()`, which silently killed four primitives):
7132                // a misspelled `withn:` would have been swallowed without a
7133                // word, and the resource would have governed nothing while
7134                // looking governed. A field the parser does not know is a field
7135                // the adopter believes in and the compiler does not.
7136                unknown => {
7137                    return Err(ParseError {
7138                        message: format!(
7139                            "Unknown field '{unknown}' in resource '{}' — expected one of: \
7140                             kind, endpoint, capacity, lifetime, certainty_floor, shield, within",
7141                            node.name
7142                        ),
7143                        line: field_tok.line,
7144                        column: field_tok.column,
7145                        ..Default::default()
7146                    });
7147                }
7148            }
7149        }
7150        self.consume(TokenType::RBrace)?;
7151        Ok(node)
7152    }
7153
7154    /// Parse: `fabric Name { provider, region, zones, ephemeral, shield }`.
7155    fn parse_fabric(&mut self) -> Result<FabricDefinition, ParseError> {
7156        let tok = self.consume(TokenType::Fabric)?;
7157        let name = self.consume(TokenType::Identifier)?.value;
7158        let mut node = FabricDefinition {
7159            name,
7160            provider: String::new(),
7161            region: String::new(),
7162            zones: None,
7163            ephemeral: None,
7164            shield_ref: String::new(),
7165            loc: Loc {
7166                line: tok.line,
7167                column: tok.column,
7168            },
7169            leading_trivia: Vec::new(),
7170            trailing_trivia: Vec::new(),
7171        };
7172        self.consume(TokenType::LBrace)?;
7173        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7174            let field_name = self.current().value.clone();
7175            self.advance();
7176            if !self.check(TokenType::Colon) {
7177                if self.check(TokenType::LBrace) {
7178                    self.skip_braced_block()?;
7179                }
7180                continue;
7181            }
7182            self.advance(); // past ':'
7183            match field_name.as_str() {
7184                "provider" => node.provider = self.consume_any_ident_or_kw()?.value,
7185                "region" => node.region = self.consume(TokenType::StringLit)?.value,
7186                "zones" => node.zones = self.parse_optional_int(),
7187                "ephemeral" => {
7188                    let b = self.parse_bool()?;
7189                    node.ephemeral = Some(b);
7190                }
7191                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
7192                _ => self.skip_value(),
7193            }
7194        }
7195        self.consume(TokenType::RBrace)?;
7196        Ok(node)
7197    }
7198
7199    /// Parse: `manifest Name { resources, fabric, region, zones, compliance }`.
7200    fn parse_manifest(&mut self) -> Result<ManifestDefinition, ParseError> {
7201        let tok = self.consume(TokenType::Manifest)?;
7202        let name = self.consume(TokenType::Identifier)?.value;
7203        let mut node = ManifestDefinition {
7204            name,
7205            resources: Vec::new(),
7206            fabric_ref: String::new(),
7207            region: String::new(),
7208            zones: None,
7209            compliance: Vec::new(),
7210            loc: Loc {
7211                line: tok.line,
7212                column: tok.column,
7213            },
7214            leading_trivia: Vec::new(),
7215            trailing_trivia: Vec::new(),
7216        };
7217        self.consume(TokenType::LBrace)?;
7218        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7219            let field_name = self.current().value.clone();
7220            self.advance();
7221            if !self.check(TokenType::Colon) {
7222                if self.check(TokenType::LBrace) {
7223                    self.skip_braced_block()?;
7224                }
7225                continue;
7226            }
7227            self.advance();
7228            match field_name.as_str() {
7229                "resources" => node.resources = self.parse_bracketed_identifiers()?,
7230                "fabric" => node.fabric_ref = self.consume_any_ident_or_kw()?.value,
7231                "region" => node.region = self.consume(TokenType::StringLit)?.value,
7232                "zones" => node.zones = self.parse_optional_int(),
7233                "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
7234                _ => self.skip_value(),
7235            }
7236        }
7237        self.consume(TokenType::RBrace)?;
7238        Ok(node)
7239    }
7240
7241    /// Parse: `observe Name from Manifest { sources, quorum, timeout, on_partition, certainty_floor }`.
7242    fn parse_observe(&mut self) -> Result<ObserveDefinition, ParseError> {
7243        let tok = self.consume(TokenType::Observe)?;
7244        let name = self.consume(TokenType::Identifier)?.value;
7245        // `from <Manifest>` — required per Python grammar.
7246        self.consume(TokenType::From)?;
7247        let target = self.consume(TokenType::Identifier)?.value;
7248        let mut node = ObserveDefinition {
7249            name,
7250            target,
7251            sources: Vec::new(),
7252            quorum: None,
7253            timeout: String::new(),
7254            on_partition: "fail".to_string(),
7255            certainty_floor: None,
7256            loc: Loc {
7257                line: tok.line,
7258                column: tok.column,
7259            },
7260            leading_trivia: Vec::new(),
7261            trailing_trivia: Vec::new(),
7262        };
7263        self.consume(TokenType::LBrace)?;
7264        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7265            let field_name = self.current().value.clone();
7266            self.advance();
7267            if !self.check(TokenType::Colon) {
7268                if self.check(TokenType::LBrace) {
7269                    self.skip_braced_block()?;
7270                }
7271                continue;
7272            }
7273            self.advance();
7274            match field_name.as_str() {
7275                "sources" => node.sources = self.parse_bracketed_identifiers()?,
7276                "quorum" => node.quorum = self.parse_optional_int(),
7277                "timeout" => {
7278                    let t = self.current().clone();
7279                    match t.ttype {
7280                        TokenType::Duration | TokenType::StringLit => {
7281                            self.advance();
7282                            node.timeout = t.value;
7283                        }
7284                        _ => node.timeout = self.consume_any_ident_or_kw()?.value,
7285                    }
7286                }
7287                "on_partition" => {
7288                    let p_tok = self.consume_any_ident_or_kw()?;
7289                    let p = p_tok.value;
7290                    if !matches!(p.as_str(), "fail" | "shield_quarantine") {
7291                        return Err(ParseError {
7292                            message: format!(
7293                                "Invalid on_partition '{p}' in observe '{}' — \
7294                                 expected fail | shield_quarantine",
7295                                node.name
7296                            ),
7297                            line: p_tok.line,
7298                            column: p_tok.column,
7299                                                    ..Default::default()
7300                        });
7301                    }
7302                    node.on_partition = p;
7303                }
7304                "certainty_floor" => node.certainty_floor = self.parse_optional_float(),
7305                _ => self.skip_value(),
7306            }
7307        }
7308        self.consume(TokenType::RBrace)?;
7309        Ok(node)
7310    }
7311
7312    // ── §λ-L-E Fase 3 — Control cognitivo ─────────────────────────
7313
7314    /// Parse: `reconcile Name { observe, threshold, tolerance, on_drift, shield, mandate, max_retries }`.
7315    fn parse_reconcile(&mut self) -> Result<ReconcileDefinition, ParseError> {
7316        let tok = self.consume(TokenType::Reconcile)?;
7317        let name = self.consume(TokenType::Identifier)?.value;
7318        let mut node = ReconcileDefinition {
7319            name,
7320            observe_ref: String::new(),
7321            threshold: None,
7322            tolerance: None,
7323            on_drift: "provision".to_string(),
7324            shield_ref: String::new(),
7325            mandate_ref: String::new(),
7326            max_retries: 3,
7327            loc: Loc {
7328                line: tok.line,
7329                column: tok.column,
7330            },
7331            leading_trivia: Vec::new(),
7332            trailing_trivia: Vec::new(),
7333        };
7334        self.consume(TokenType::LBrace)?;
7335        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7336            let field_name = self.current().value.clone();
7337            self.advance();
7338            if !self.check(TokenType::Colon) {
7339                if self.check(TokenType::LBrace) {
7340                    self.skip_braced_block()?;
7341                }
7342                continue;
7343            }
7344            self.advance();
7345            match field_name.as_str() {
7346                "observe" => node.observe_ref = self.consume_any_ident_or_kw()?.value,
7347                "threshold" => node.threshold = self.parse_optional_float(),
7348                "tolerance" => node.tolerance = self.parse_optional_float(),
7349                "on_drift" => {
7350                    let d_tok = self.consume_any_ident_or_kw()?;
7351                    let d = d_tok.value;
7352                    if !matches!(d.as_str(), "provision" | "alert" | "refine") {
7353                        return Err(ParseError {
7354                            message: format!(
7355                                "Invalid on_drift '{d}' in reconcile '{}' — \
7356                                 expected provision | alert | refine",
7357                                node.name
7358                            ),
7359                            line: d_tok.line,
7360                            column: d_tok.column,
7361                                                    ..Default::default()
7362                        });
7363                    }
7364                    node.on_drift = d;
7365                }
7366                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
7367                "mandate" => node.mandate_ref = self.consume_any_ident_or_kw()?.value,
7368                "max_retries" => {
7369                    if let Some(v) = self.parse_optional_int() {
7370                        node.max_retries = v;
7371                    }
7372                }
7373                _ => self.skip_value(),
7374            }
7375        }
7376        self.consume(TokenType::RBrace)?;
7377        Ok(node)
7378    }
7379
7380    /// Parse: `lease Name { resource, duration, acquire, on_expire }`.
7381    fn parse_lease(&mut self) -> Result<LeaseDefinition, ParseError> {
7382        let tok = self.consume(TokenType::Lease)?;
7383        let name = self.consume(TokenType::Identifier)?.value;
7384        let mut node = LeaseDefinition {
7385            name,
7386            resource_ref: String::new(),
7387            duration: String::new(),
7388            acquire: "on_start".to_string(),
7389            on_expire: "anchor_breach".to_string(),
7390            loc: Loc {
7391                line: tok.line,
7392                column: tok.column,
7393            },
7394            leading_trivia: Vec::new(),
7395            trailing_trivia: Vec::new(),
7396        };
7397        self.consume(TokenType::LBrace)?;
7398        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7399            let field_name = self.current().value.clone();
7400            self.advance();
7401            if !self.check(TokenType::Colon) {
7402                if self.check(TokenType::LBrace) {
7403                    self.skip_braced_block()?;
7404                }
7405                continue;
7406            }
7407            self.advance();
7408            match field_name.as_str() {
7409                "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
7410                "duration" => {
7411                    let t = self.current().clone();
7412                    match t.ttype {
7413                        TokenType::Duration | TokenType::StringLit => {
7414                            self.advance();
7415                            node.duration = t.value;
7416                        }
7417                        _ => node.duration = self.consume_any_ident_or_kw()?.value,
7418                    }
7419                }
7420                "acquire" => {
7421                    let a_tok = self.consume_any_ident_or_kw()?;
7422                    let a = a_tok.value;
7423                    if !matches!(a.as_str(), "on_start" | "on_demand") {
7424                        return Err(ParseError {
7425                            message: format!(
7426                                "Invalid acquire '{a}' in lease '{}' — \
7427                                 expected on_start | on_demand",
7428                                node.name
7429                            ),
7430                            line: a_tok.line,
7431                            column: a_tok.column,
7432                                                    ..Default::default()
7433                        });
7434                    }
7435                    node.acquire = a;
7436                }
7437                "on_expire" => {
7438                    let e_tok = self.consume_any_ident_or_kw()?;
7439                    let e = e_tok.value;
7440                    if !matches!(e.as_str(), "anchor_breach" | "release" | "extend") {
7441                        return Err(ParseError {
7442                            message: format!(
7443                                "Invalid on_expire '{e}' in lease '{}' — \
7444                                 expected anchor_breach | release | extend",
7445                                node.name
7446                            ),
7447                            line: e_tok.line,
7448                            column: e_tok.column,
7449                                                    ..Default::default()
7450                        });
7451                    }
7452                    node.on_expire = e;
7453                }
7454                _ => self.skip_value(),
7455            }
7456        }
7457        self.consume(TokenType::RBrace)?;
7458        Ok(node)
7459    }
7460
7461    /// Parse: `ensemble Name { observations, quorum, aggregation, certainty_mode }`.
7462    fn parse_ensemble(&mut self) -> Result<EnsembleDefinition, ParseError> {
7463        let tok = self.consume(TokenType::Ensemble)?;
7464        let name = self.consume(TokenType::Identifier)?.value;
7465        let mut node = EnsembleDefinition {
7466            name,
7467            observations: Vec::new(),
7468            quorum: None,
7469            aggregation: "majority".to_string(),
7470            certainty_mode: "min".to_string(),
7471            loc: Loc {
7472                line: tok.line,
7473                column: tok.column,
7474            },
7475            leading_trivia: Vec::new(),
7476            trailing_trivia: Vec::new(),
7477        };
7478        self.consume(TokenType::LBrace)?;
7479        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7480            let field_name = self.current().value.clone();
7481            self.advance();
7482            if !self.check(TokenType::Colon) {
7483                if self.check(TokenType::LBrace) {
7484                    self.skip_braced_block()?;
7485                }
7486                continue;
7487            }
7488            self.advance();
7489            match field_name.as_str() {
7490                "observations" => node.observations = self.parse_bracketed_identifiers()?,
7491                "quorum" => node.quorum = self.parse_optional_int(),
7492                "aggregation" => {
7493                    let a_tok = self.consume_any_ident_or_kw()?;
7494                    let a = a_tok.value;
7495                    if !matches!(a.as_str(), "majority" | "weighted" | "byzantine") {
7496                        return Err(ParseError {
7497                            message: format!(
7498                                "Invalid aggregation '{a}' in ensemble '{}' — \
7499                                 expected majority | weighted | byzantine",
7500                                node.name
7501                            ),
7502                            line: a_tok.line,
7503                            column: a_tok.column,
7504                                                    ..Default::default()
7505                        });
7506                    }
7507                    node.aggregation = a;
7508                }
7509                "certainty_mode" => {
7510                    let c_tok = self.consume_any_ident_or_kw()?;
7511                    let c = c_tok.value;
7512                    if !matches!(c.as_str(), "min" | "weighted" | "harmonic") {
7513                        return Err(ParseError {
7514                            message: format!(
7515                                "Invalid certainty_mode '{c}' in ensemble '{}' — \
7516                                 expected min | weighted | harmonic",
7517                                node.name
7518                            ),
7519                            line: c_tok.line,
7520                            column: c_tok.column,
7521                                                    ..Default::default()
7522                        });
7523                    }
7524                    node.certainty_mode = c;
7525                }
7526                _ => self.skip_value(),
7527            }
7528        }
7529        self.consume(TokenType::RBrace)?;
7530        Ok(node)
7531    }
7532
7533    // ── §λ-L-E Fase 4 — Topology + π-calculus binary sessions ─────
7534
7535    /// Parse: `session Name { role1: [step, …]  role2: [step, …] }`.
7536    ///
7537    /// The enclosing `parse_session_definition` disambiguates from the session
7538    /// step token `session` (which does not exist) by always entering from the
7539    /// top-level dispatch; the identifier role name is consumed after `{`.
7540    fn parse_session_definition(&mut self) -> Result<SessionDefinition, ParseError> {
7541        let tok = self.consume(TokenType::Session)?;
7542        let name = self.consume(TokenType::Identifier)?.value;
7543        let mut node = SessionDefinition {
7544            name,
7545            roles: Vec::new(),
7546            loc: Loc {
7547                line: tok.line,
7548                column: tok.column,
7549            },
7550            leading_trivia: Vec::new(),
7551            trailing_trivia: Vec::new(),
7552        };
7553        self.consume(TokenType::LBrace)?;
7554        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7555            let role_tok = self.consume_any_ident_or_kw()?;
7556            self.consume(TokenType::Colon)?;
7557            let steps = self.parse_session_steps()?;
7558            node.roles.push(SessionRole {
7559                name: role_tok.value,
7560                steps,
7561                loc: Loc {
7562                    line: role_tok.line,
7563                    column: role_tok.column,
7564                },
7565            });
7566        }
7567        self.consume(TokenType::RBrace)?;
7568        Ok(node)
7569    }
7570
7571    /// §Fase 51.c.2 — Parse a Pauli-sum observable declaration:
7572    /// ```text
7573    /// observable EnergyHamiltonian {
7574    ///     qubits: 2
7575    ///     term: 0.5 * "ZZ"
7576    ///     term: -1.2 * "XI"
7577    /// }
7578    /// ```
7579    /// `term:` is a repeatable key (one `cₖ · Pₖ` per line). The coefficient is
7580    /// a real scalar (optional leading `+`/`-`), then `*`, then a quoted Pauli
7581    /// string. The type-checker (§51.c.2) validates the closed `{I,X,Y,Z}`
7582    /// alphabet + equal lengths; real coefficients ⇒ Hermitian by construction.
7583    fn parse_observable(&mut self) -> Result<ObservableDefinition, ParseError> {
7584        let tok = self.consume(TokenType::Observable)?;
7585        let name = self.consume(TokenType::Identifier)?.value;
7586        let mut node = ObservableDefinition {
7587            name,
7588            qubits: None,
7589            terms: Vec::new(),
7590            loc: Loc {
7591                line: tok.line,
7592                column: tok.column,
7593            },
7594            leading_trivia: Vec::new(),
7595            trailing_trivia: Vec::new(),
7596        };
7597        self.consume(TokenType::LBrace)?;
7598        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7599            let key_tok = self.consume_any_ident_or_kw()?;
7600            self.consume(TokenType::Colon)?;
7601            match key_tok.value.as_str() {
7602                "qubits" => node.qubits = Some(self.consume_number()? as i64),
7603                "term" => {
7604                    let term_loc = Loc {
7605                        line: key_tok.line,
7606                        column: key_tok.column,
7607                    };
7608                    // Optional sign, then magnitude.
7609                    let mut negative = false;
7610                    if self.check(TokenType::Minus) {
7611                        self.advance();
7612                        negative = true;
7613                    } else if self.check(TokenType::Plus) {
7614                        self.advance();
7615                    }
7616                    let mag = self.consume_number()?;
7617                    let coefficient = if negative { -mag } else { mag };
7618                    // `*` separator between coefficient and Pauli string.
7619                    self.consume(TokenType::Star)?;
7620                    let pauli = self.consume(TokenType::StringLit)?.value;
7621                    node.terms.push(PauliTerm {
7622                        coefficient,
7623                        pauli,
7624                        loc: term_loc,
7625                    });
7626                }
7627                _ => self.skip_value(),
7628            }
7629        }
7630        self.consume(TokenType::RBrace)?;
7631        Ok(node)
7632    }
7633
7634    /// §Fase 69.a — Parse:
7635    /// `witness Name { claim: <ref>  against: <baseline>  metric: <metric>
7636    ///                 threshold: <ε>  data: <source> }`.
7637    /// Order-free `key: value` pairs. `claim`/`against`/`metric`/`data` are bare
7638    /// identifiers (a ref or a closed-catalog keyword); `threshold` is a number.
7639    /// Well-formedness (known metric, threshold range, required fields) is the
7640    /// type-checker's job (`axon-E0790`).
7641    fn parse_witness(&mut self) -> Result<WitnessDefinition, ParseError> {
7642        let tok = self.consume(TokenType::Witness)?;
7643        let name = self.consume(TokenType::Identifier)?.value;
7644        let mut node = WitnessDefinition {
7645            name,
7646            claim: String::new(),
7647            baseline: String::new(),
7648            metric: String::new(),
7649            threshold: 0.0,
7650            data: String::new(),
7651            loc: Loc {
7652                line: tok.line,
7653                column: tok.column,
7654            },
7655            leading_trivia: Vec::new(),
7656            trailing_trivia: Vec::new(),
7657        };
7658        self.consume(TokenType::LBrace)?;
7659        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7660            let key_tok = self.consume_any_ident_or_kw()?;
7661            self.consume(TokenType::Colon)?;
7662            match key_tok.value.as_str() {
7663                "claim" => node.claim = self.consume_any_ident_or_kw()?.value,
7664                // `against` is the baseline; `against` is not a reserved keyword,
7665                // so it lexes as an identifier key here.
7666                "against" => node.baseline = self.consume_any_ident_or_kw()?.value,
7667                "metric" => node.metric = self.consume_any_ident_or_kw()?.value,
7668                "threshold" => node.threshold = self.consume_number()?,
7669                "data" => node.data = self.consume_any_ident_or_kw()?.value,
7670                _ => self.skip_value(),
7671            }
7672        }
7673        self.consume(TokenType::RBrace)?;
7674        Ok(node)
7675    }
7676
7677    /// §Fase 41.b — Parse:
7678    /// `socket Name { protocol: SessionRef, backpressure: credit(n),
7679    ///               reconnect: cognitive_state, legal_basis: ... }`.
7680    /// Fields are `key: value` pairs (order-free); only `protocol` is required.
7681    fn parse_socket(&mut self) -> Result<SocketDefinition, ParseError> {
7682        let tok = self.consume(TokenType::Socket)?;
7683        let name = self.consume(TokenType::Identifier)?.value;
7684        let mut node = SocketDefinition {
7685            name,
7686            loc: Loc { line: tok.line, column: tok.column },
7687            ..Default::default()
7688        };
7689        self.consume(TokenType::LBrace)?;
7690        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7691            let key = self.consume_any_ident_or_kw()?.value;
7692            self.consume(TokenType::Colon)?;
7693            match key.as_str() {
7694                "protocol" => node.protocol = self.consume_any_ident_or_kw()?.value,
7695                "backpressure" => {
7696                    // `credit(n)` — the typed-resource window.
7697                    let kind = self.consume_any_ident_or_kw()?.value;
7698                    if kind != "credit" {
7699                        return Err(self.error(&format!("expected `credit(n)` for backpressure, got `{kind}`")));
7700                    }
7701                    self.consume(TokenType::LParen)?;
7702                    let n = self
7703                        .consume(TokenType::Integer)?
7704                        .value
7705                        .parse::<i64>()
7706                        .map_err(|_| self.error("backpressure credit must be an integer"))?;
7707                    self.consume(TokenType::RParen)?;
7708                    node.backpressure_credit = Some(n);
7709                }
7710                "reconnect" => {
7711                    let mode = self.consume_any_ident_or_kw()?.value;
7712                    node.reconnect = mode == "cognitive_state";
7713                }
7714                "legal_basis" => node.legal_basis = Some(self.consume_any_ident_or_kw()?.value),
7715                other => return Err(self.error(&format!("unknown socket field `{other}`"))),
7716            }
7717            // Optional comma between fields.
7718            if self.check(TokenType::Comma) {
7719                self.consume(TokenType::Comma)?;
7720            }
7721        }
7722        self.consume(TokenType::RBrace)?;
7723        Ok(node)
7724    }
7725
7726    /// §Fase 80.b — parse `upstream Name [from Preset@vN] { fields }`.
7727    ///
7728    /// Field grammar per `docs/fase/fase_80_upstream_design.md` §1–2. The
7729    /// parser fixes the *shape* only; catalog membership (`transport:`,
7730    /// `auth:`, `overflow:`, `on_exhausted:`), key charsets and projection
7731    /// totality are §80.c type-checker laws (T849–T851), mirroring how
7732    /// `socket` splits parse vs. check.
7733    fn parse_upstream(&mut self) -> Result<UpstreamDefinition, ParseError> {
7734        let tok = self.consume(TokenType::Upstream)?;
7735        let name = self.consume(TokenType::Identifier)?.value;
7736        let mut node = UpstreamDefinition {
7737            name,
7738            loc: Loc { line: tok.line, column: tok.column },
7739            ..Default::default()
7740        };
7741        // §80.f — preset instantiation: `upstream X from DeepgramSTT@v1 {…}`.
7742        if self.check(TokenType::From) {
7743            self.advance();
7744            let base = self.consume(TokenType::Identifier)?.value;
7745            self.consume(TokenType::At)?;
7746            let version = self.consume_any_ident_or_kw()?.value;
7747            node.preset = Some(format!("{base}@{version}"));
7748        }
7749        self.consume(TokenType::LBrace)?;
7750        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7751            let key = self.consume_any_ident_or_kw()?.value;
7752            self.consume(TokenType::Colon)?;
7753            match key.as_str() {
7754                "transport" => node.transport = self.consume_any_ident_or_kw()?.value,
7755                "protocol" => node.protocol = self.consume_any_ident_or_kw()?.value,
7756                "role" => node.role = self.consume_any_ident_or_kw()?.value,
7757                "resolve" => node.resolve = self.parse_dotted_identifier()?,
7758                // §Fase 114.u — the upstream's channel rides a declared
7759                // `resource`; the address + instance bound DERIVE from it.
7760                // XOR with `resolve:` is axon-T951 (type-checker territory).
7761                "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
7762                "secret" => node.secret = self.parse_dotted_identifier()?,
7763                "auth" => {
7764                    // `header("Name")` | `header("Name", "Prefix ")` |
7765                    // `query("param")` | `signed_url`.
7766                    node.auth_kind = self.consume_any_ident_or_kw()?.value;
7767                    if self.check(TokenType::LParen) {
7768                        self.consume(TokenType::LParen)?;
7769                        node.auth_name = Some(self.consume(TokenType::StringLit)?.value);
7770                        if self.check(TokenType::Comma) {
7771                            self.consume(TokenType::Comma)?;
7772                            node.auth_prefix = Some(self.consume(TokenType::StringLit)?.value);
7773                        }
7774                        self.consume(TokenType::RParen)?;
7775                    }
7776                }
7777                "map" => node.map = self.parse_upstream_map()?,
7778                "reconnect" => node.reconnect = Some(self.parse_upstream_reconnect()?),
7779                "overflow" => node.overflow = Some(self.consume_any_ident_or_kw()?.value),
7780                "backpressure" => {
7781                    // `credit(n)` — same typed-resource window as `socket`.
7782                    let kind = self.consume_any_ident_or_kw()?.value;
7783                    if kind != "credit" {
7784                        return Err(self.error(&format!("expected `credit(n)` for backpressure, got `{kind}`")));
7785                    }
7786                    self.consume(TokenType::LParen)?;
7787                    let n = self
7788                        .consume(TokenType::Integer)?
7789                        .value
7790                        .parse::<i64>()
7791                        .map_err(|_| self.error("backpressure credit must be an integer"))?;
7792                    self.consume(TokenType::RParen)?;
7793                    node.backpressure_credit = Some(n);
7794                }
7795                other => return Err(self.error(&format!("unknown upstream field `{other}`"))),
7796            }
7797            // Optional comma between fields.
7798            if self.check(TokenType::Comma) {
7799                self.consume(TokenType::Comma)?;
7800            }
7801        }
7802        self.consume(TokenType::RBrace)?;
7803        Ok(node)
7804    }
7805
7806    /// §Fase 83.a — parse `cors Name { fields }`. Field-shape checks
7807    /// (wildcard+credentials, origin-glob shape, closed method catalog,
7808    /// cross-method path consistency) are §83.c type-checker territory
7809    /// (T853-T857); the parser only builds the structural AST.
7810    ///
7811    /// **Unknown fields are a hard error** (D83.7, not `shield`'s lenient
7812    /// `axon-W010` record-and-skip) — mirrors `upstream`'s stricter
7813    /// posture, appropriate for a security-relevant declaration.
7814    fn parse_cors(&mut self) -> Result<CorsDefinition, ParseError> {
7815        let tok = self.consume(TokenType::Cors)?;
7816        let name = self.consume(TokenType::Identifier)?.value;
7817        let mut node = CorsDefinition {
7818            name,
7819            loc: Loc { line: tok.line, column: tok.column },
7820            ..Default::default()
7821        };
7822        self.consume(TokenType::LBrace)?;
7823        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7824            let key = self.consume_any_ident_or_kw()?.value;
7825            self.consume(TokenType::Colon)?;
7826            match key.as_str() {
7827                "allow_origins" => node.allow_origins = self.parse_bracketed_strings()?,
7828                "allow_methods" => node.allow_methods = self.parse_bracketed_identifiers()?,
7829                "allow_headers" => node.allow_headers = self.parse_bracketed_strings()?,
7830                "allow_credentials" => {
7831                    node.allow_credentials = self.consume_any_ident_or_kw()?.value == "true"
7832                }
7833                "max_age" => node.max_age = Some(self.consume(TokenType::Duration)?.value),
7834                "expose_headers" => node.expose_headers = self.parse_bracketed_strings()?,
7835                other => return Err(self.error(&format!("unknown cors field `{other}`"))),
7836            }
7837            // Optional comma between fields.
7838            if self.check(TokenType::Comma) {
7839                self.consume(TokenType::Comma)?;
7840            }
7841        }
7842        self.consume(TokenType::RBrace)?;
7843        Ok(node)
7844    }
7845
7846    /// §Fase 92.a — parse `credential Name { ttl: grants: }`. Strict
7847    /// closed-catalog (unknown field is a hard error, the §83 D83.7
7848    /// discipline — a credential contract governs AUTHORITY, so a typo can
7849    /// never silently produce a permissive contract). `grants:` slugs are
7850    /// validated at parse time with the same closed grammar as
7851    /// `axonendpoint requires:`; the cross-field laws (non-empty grants,
7852    /// TTL bounds) are §92.a type-checker territory (`axon-T893`/`T894`).
7853    fn parse_credential(&mut self) -> Result<CredentialDefinition, ParseError> {
7854        let tok = self.consume(TokenType::Credential)?;
7855        let name = self.consume(TokenType::Identifier)?.value;
7856        let mut node = CredentialDefinition {
7857            name,
7858            loc: Loc { line: tok.line, column: tok.column },
7859            ..Default::default()
7860        };
7861        self.consume(TokenType::LBrace)?;
7862        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7863            let key = self.consume_any_ident_or_kw()?.value;
7864            self.consume(TokenType::Colon)?;
7865            match key.as_str() {
7866                "ttl" => node.ttl = self.consume(TokenType::Duration)?.value,
7867                "grants" => {
7868                    let bracket_tok = self.current().clone();
7869                    let items = self.parse_bracketed_dot_identifiers()?;
7870                    for slug in &items {
7871                        if !is_valid_capability_slug(slug) {
7872                            return Err(ParseError {
7873                                message: format!(
7874                                    "Invalid capability slug '{slug}' in credential '{}' \
7875                                     `grants:`. Capability slugs must match \
7876                                     ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
7877                                     lowercase identifiers starting with a letter. Examples: \
7878                                     `chat.invoke`, `flow.execute`.",
7879                                    node.name
7880                                ),
7881                                line: bracket_tok.line,
7882                                column: bracket_tok.column,
7883                                ..Default::default()
7884                            });
7885                        }
7886                    }
7887                    node.grants = items;
7888                }
7889                other => return Err(self.error(&format!("unknown credential field `{other}`"))),
7890            }
7891            // Optional comma between fields.
7892            if self.check(TokenType::Comma) {
7893                self.consume(TokenType::Comma)?;
7894            }
7895        }
7896        self.consume(TokenType::RBrace)?;
7897        Ok(node)
7898    }
7899
7900    /// §Fase 85.a — parse `cache Name { backend:, ttl:, key:, default:,
7901    /// apply_to_effects:, invalidate_on: }`. Strict closed-catalog (unknown
7902    /// field is a hard error, the §83 D83.7 discipline — a cache governs
7903    /// correctness, so a typo can never silently mean "no policy"). All
7904    /// cross-field laws (single default, non-pure-needs-ttl, reference
7905    /// resolution, effect widening) are §85.c type-checker territory.
7906    fn parse_cache(&mut self) -> Result<CacheDefinition, ParseError> {
7907        let tok = self.consume(TokenType::Cache)?;
7908        let name = self.consume(TokenType::Identifier)?.value;
7909        let mut node = CacheDefinition {
7910            name,
7911            loc: Loc { line: tok.line, column: tok.column },
7912            ..Default::default()
7913        };
7914        self.consume(TokenType::LBrace)?;
7915        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7916            let key = self.consume_any_ident_or_kw()?.value;
7917            self.consume(TokenType::Colon)?;
7918            match key.as_str() {
7919                "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
7920                "ttl" => node.ttl = Some(self.consume(TokenType::Duration)?.value),
7921                "key" => node.key_params = self.parse_bracketed_identifiers()?,
7922                "default" => {
7923                    node.default_policy = self.consume_any_ident_or_kw()?.value == "true"
7924                }
7925                "apply_to_effects" => {
7926                    node.apply_to_effects = self.parse_bracketed_identifiers()?
7927                }
7928                "invalidate_on" => node.invalidate_on = self.parse_bracketed_identifiers()?,
7929                other => return Err(self.error(&format!("unknown cache field `{other}`"))),
7930            }
7931            if self.check(TokenType::Comma) {
7932                self.consume(TokenType::Comma)?;
7933            }
7934        }
7935        self.consume(TokenType::RBrace)?;
7936        Ok(node)
7937    }
7938
7939    // ── §Fase 99.b — Native Document Synthesis ─────────────────────────────
7940
7941    /// §Fase 99.b — parse `document <Name> { target:, template:?, provenance:?,
7942    /// effects:?, <body blocks> }`. Document-level scalars are handled here;
7943    /// anything of the form `ident { … }` is a body block ([`parse_doc_block_body`]).
7944    /// Unknown scalar fields are a hard error (the §83/§84 closed-catalog
7945    /// discipline); the per-`target` block vocabulary is the §99.c checker's job.
7946    fn parse_document(&mut self) -> Result<crate::ast::DocumentDefinition, ParseError> {
7947        let tok = self.consume(TokenType::Document)?;
7948        let name = self.consume(TokenType::Identifier)?.value;
7949        let mut node = crate::ast::DocumentDefinition {
7950            name,
7951            loc: Loc {
7952                line: tok.line,
7953                column: tok.column,
7954            },
7955            ..Default::default()
7956        };
7957        self.consume(TokenType::LBrace)?;
7958        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7959            let field = self.current().clone();
7960            let field_name = field.value.clone();
7961            self.advance();
7962            if self.check(TokenType::Colon) {
7963                self.advance();
7964                match field_name.as_str() {
7965                    "target" => node.target = self.consume_any_ident_or_kw()?.value,
7966                    "template" => node.template = self.parse_dotted_identifier()?,
7967                    "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
7968                    "effects" => node.effects = Some(self.parse_effect_row()?),
7969                    other => {
7970                        return Err(self.error(&format!(
7971                            "unknown document field `{other}` in document `{}` — expected \
7972                             `target:` / `template:` / `provenance:` / `effects:`, or a body \
7973                             block (`section {{ … }}` / `slide {{ … }}` / `sheet {{ … }}`)",
7974                            node.name
7975                        )))
7976                    }
7977                }
7978            } else if self.check(TokenType::LBrace) {
7979                node.blocks
7980                    .push(self.parse_doc_block_body(field_name, field.line, field.column)?);
7981            } else {
7982                return Err(self.error(&format!(
7983                    "unexpected `{field_name}` in document `{}` body — expected a `field:` or a \
7984                     body block `{field_name} {{ … }}`",
7985                    node.name
7986                )));
7987            }
7988            if self.check(TokenType::Comma) {
7989                self.advance();
7990            }
7991        }
7992        self.consume(TokenType::RBrace)?;
7993        Ok(node)
7994    }
7995
7996    /// §Fase 99.b — parse a document body block whose `kind` was already
7997    /// consumed: `{ (field: value | nested-block { … })* }`. Recursive — a
7998    /// `section` holds `para`/`table`/`chart`; a `slide` holds `bullets`/
7999    /// `notes`; a `sheet` holds `row`/`formula`. A member is a field iff a
8000    /// `:` follows its name; else it must open a nested block (`{`).
8001    fn parse_doc_block_body(
8002        &mut self,
8003        kind: String,
8004        line: u32,
8005        column: u32,
8006    ) -> Result<crate::ast::DocBlock, ParseError> {
8007        let mut block = crate::ast::DocBlock {
8008            kind,
8009            loc: Loc { line, column },
8010            ..Default::default()
8011        };
8012        self.consume(TokenType::LBrace)?;
8013        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8014            let name_tok = self.current().clone();
8015            let name = self.consume_any_ident_or_kw()?.value;
8016            if self.check(TokenType::Colon) {
8017                self.advance();
8018                let value = self.parse_doc_scalar()?;
8019                block.fields.push((name, value));
8020            } else if self.check(TokenType::LBrace) {
8021                let child = self.parse_doc_block_body(name, name_tok.line, name_tok.column)?;
8022                block.children.push(child);
8023            } else {
8024                return Err(self.error(&format!(
8025                    "in document block `{}`: `{name}` must be a `field:` value or open a nested \
8026                     block `{name} {{ … }}`",
8027                    block.kind
8028                )));
8029            }
8030            if self.check(TokenType::Comma) {
8031                self.advance();
8032            }
8033        }
8034        self.consume(TokenType::RBrace)?;
8035        Ok(block)
8036    }
8037
8038    /// §Fase 99.b — parse a document field value into a [`crate::ast::DocScalar`].
8039    /// A bare identifier is a REFERENCE (`text: revenue_summary`) — this is what
8040    /// the assertion-laundering barrier inspects; a quoted string / int / bool /
8041    /// bracketed list are literals.
8042    fn parse_doc_scalar(&mut self) -> Result<crate::ast::DocScalar, ParseError> {
8043        let tok = self.current().clone();
8044        match tok.ttype {
8045            TokenType::StringLit => {
8046                self.advance();
8047                Ok(crate::ast::DocScalar::Text(tok.value))
8048            }
8049            TokenType::Integer => {
8050                self.advance();
8051                Ok(crate::ast::DocScalar::Int(tok.value.parse::<i64>().unwrap_or(0)))
8052            }
8053            TokenType::Bool => {
8054                self.advance();
8055                Ok(crate::ast::DocScalar::Bool(tok.value == "true"))
8056            }
8057            TokenType::LBracket => {
8058                let items = self.parse_bracketed_strings()?;
8059                Ok(crate::ast::DocScalar::List(items))
8060            }
8061            _ => {
8062                let name = self.consume_any_ident_or_kw()?.value;
8063                Ok(crate::ast::DocScalar::Ref(name))
8064            }
8065        }
8066    }
8067
8068    // ── §Fase 105 — Governed CRM Delivery ──────────────────────────────────
8069
8070    /// §Fase 105 — parse `deliver <Name> { target:, provenance:?, secret:,
8071    /// effects:?, <operation blocks> }`. Delivery-level scalars are handled here;
8072    /// anything of the form `ident { … }` is an operation block
8073    /// ([`parse_deliver_op`]). Unknown scalar fields are a hard error (the §99
8074    /// §Fase 110.a — the governed human-notification declaration:
8075    ///
8076    /// ```text
8077    /// notify LowSales {
8078    ///     channel:    sms | whatsapp | telegram
8079    ///     to:         secret(ops.oncall_phone)
8080    ///     template:   "Ventas 7d: ${resumen}"
8081    ///     window:     4h
8082    ///     provenance: attached | cleared
8083    ///     effects:    <web>
8084    /// }
8085    /// ```
8086    ///
8087    /// The closed-field discipline (§99/§105): an unknown scalar field is
8088    /// a hard parse error. The LAWS (T933/T934/T935) live in the checker
8089    /// so violations accumulate; the parser records shape (including a
8090    /// literal `to:` — kept so T934 can refuse it TEACHING the custody
8091    /// form, instead of a bare parse error).
8092    fn parse_notify(&mut self) -> Result<crate::ast::NotifyDefinition, ParseError> {
8093        let tok = self.consume(TokenType::Notify)?;
8094        let name = self.consume(TokenType::Identifier)?.value;
8095        let mut node = crate::ast::NotifyDefinition {
8096            name,
8097            loc: Loc {
8098                line: tok.line,
8099                column: tok.column,
8100            },
8101            ..Default::default()
8102        };
8103        self.consume(TokenType::LBrace)?;
8104        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8105            let field = self.current().clone();
8106            let field_name = field.value.clone();
8107            self.advance();
8108            if self.check(TokenType::Colon) {
8109                self.advance();
8110                match field_name.as_str() {
8111                    "channel" => node.channel = self.consume_any_ident_or_kw()?.value,
8112                    "to" => {
8113                        // The custody form: `secret(<dotted-class>)`. A string
8114                        // literal parses too — the checker refuses it (T934)
8115                        // with the teaching message.
8116                        if self.current().value == "secret" && self.peek_is_lparen() {
8117                            self.advance(); // `secret`
8118                            self.consume(TokenType::LParen)?;
8119                            node.to_secret = self.parse_dotted_identifier()?;
8120                            self.consume(TokenType::RParen)?;
8121                            node.to_is_secret = true;
8122                        } else if self.check(TokenType::StringLit) {
8123                            node.to_secret = self.consume(TokenType::StringLit)?.value.clone();
8124                            node.to_is_secret = false;
8125                        } else {
8126                            node.to_secret = self.consume_any_ident_or_kw()?.value.clone();
8127                            node.to_is_secret = false;
8128                        }
8129                    }
8130                    "template" => {
8131                        node.template = self.consume(TokenType::StringLit)?.value.clone()
8132                    }
8133                    "window" => {
8134                        // `4h` lexes as Integer + ident or one ident — accept
8135                        // both spellings, normalized to the joined form.
8136                        if self.check(TokenType::Integer) {
8137                            let n = self.consume(TokenType::Integer)?.value.clone();
8138                            let unit = self.consume_any_ident_or_kw()?.value.clone();
8139                            node.window = format!("{n}{unit}");
8140                        } else {
8141                            node.window = self.consume_any_ident_or_kw()?.value.clone();
8142                        }
8143                    }
8144                    "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
8145                    "effects" => node.effects = Some(self.parse_effect_row()?),
8146                    other => {
8147                        return Err(self.error(&format!(
8148                            "unknown notify field `{other}` in notify `{}` — expected \
8149                             `channel:` / `to:` / `template:` / `window:` / `provenance:` / \
8150                             `effects:`",
8151                            node.name
8152                        )))
8153                    }
8154                }
8155            }
8156        }
8157        self.consume(TokenType::RBrace)?;
8158        Ok(node)
8159    }
8160
8161    /// §Fase 110.a — one-token lookahead helper for the `secret(` form.
8162    /// §Fase 114.a — is the NEXT token an identifier? (`budget <Name> { … }` vs
8163    /// a bare `budget` used as an ordinary identifier.)
8164    fn peek_is_identifier(&self) -> bool {
8165        self.tokens
8166            .get(self.pos + 1)
8167            .map(|t| t.ttype == TokenType::Identifier)
8168            .unwrap_or(false)
8169    }
8170
8171    fn peek_is_lparen(&self) -> bool {
8172        self.tokens
8173            .get(self.pos + 1)
8174            .map(|t| t.ttype == TokenType::LParen)
8175            .unwrap_or(false)
8176    }
8177
8178    /// closed-catalog discipline); the operation vocabulary is the checker's job.
8179    fn parse_deliver(&mut self) -> Result<crate::ast::DeliverDefinition, ParseError> {
8180        let tok = self.consume(TokenType::Deliver)?;
8181        let name = self.consume(TokenType::Identifier)?.value;
8182        let mut node = crate::ast::DeliverDefinition {
8183            name,
8184            loc: Loc {
8185                line: tok.line,
8186                column: tok.column,
8187            },
8188            ..Default::default()
8189        };
8190        self.consume(TokenType::LBrace)?;
8191        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8192            let field = self.current().clone();
8193            let field_name = field.value.clone();
8194            self.advance();
8195            if self.check(TokenType::Colon) {
8196                self.advance();
8197                match field_name.as_str() {
8198                    "target" => node.target = self.consume_any_ident_or_kw()?.value,
8199                    "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
8200                    "secret" => node.secret = self.consume_any_ident_or_kw()?.value,
8201                    "effects" => node.effects = Some(self.parse_effect_row()?),
8202                    other => {
8203                        return Err(self.error(&format!(
8204                            "unknown deliver field `{other}` in deliver `{}` — expected \
8205                             `target:` / `provenance:` / `secret:` / `effects:`, or an operation \
8206                             block (`upsert_contact {{ … }}` / `create_deal {{ … }}` / \
8207                             `add_note {{ … }}`)",
8208                            node.name
8209                        )))
8210                    }
8211                }
8212            } else if self.check(TokenType::LBrace) {
8213                node.ops
8214                    .push(self.parse_deliver_op(field_name, field.line, field.column)?);
8215            } else {
8216                return Err(self.error(&format!(
8217                    "unexpected `{field_name}` in deliver `{}` body — expected a `field:` or an \
8218                     operation block `{field_name} {{ … }}`",
8219                    node.name
8220                )));
8221            }
8222            if self.check(TokenType::Comma) {
8223                self.advance();
8224            }
8225        }
8226        self.consume(TokenType::RBrace)?;
8227        Ok(node)
8228    }
8229
8230    /// §Fase 105 — parse a delivery operation block whose `kind` was already
8231    /// consumed: `{ (field: value)* }`. Flat (unlike a document block, an
8232    /// operation has no nested children) — each member must be a `field: value`.
8233    fn parse_deliver_op(
8234        &mut self,
8235        kind: String,
8236        line: u32,
8237        column: u32,
8238    ) -> Result<crate::ast::DeliverOp, ParseError> {
8239        let mut op = crate::ast::DeliverOp {
8240            kind,
8241            loc: Loc { line, column },
8242            ..Default::default()
8243        };
8244        self.consume(TokenType::LBrace)?;
8245        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8246            let name = self.consume_any_ident_or_kw()?.value;
8247            self.consume(TokenType::Colon).map_err(|_| {
8248                self.error(&format!(
8249                    "in deliver operation `{}`: `{name}` must be a `field: value` pair — an \
8250                     operation binds scalar fields, it takes no nested blocks",
8251                    op.kind
8252                ))
8253            })?;
8254            let value = self.parse_doc_scalar()?;
8255            op.fields.push((name, value));
8256            if self.check(TokenType::Comma) {
8257                self.advance();
8258            }
8259        }
8260        self.consume(TokenType::RBrace)?;
8261        Ok(op)
8262    }
8263
8264    /// §Fase 87.a — parse `savant <Name> { domain:, cognition{…}, memory{…},
8265    /// budget{…}, mandate <M> {…} … }`. The block surface only; catalog +
8266    /// ref-resolution + budget/interruptibility binding is the §87.b/c checker's
8267    /// job (the standing parse/check split). Unknown fields are a hard error
8268    /// (D83.7): a savant governs an expensive autonomous process.
8269    fn parse_savant(&mut self) -> Result<SavantDefinition, ParseError> {
8270        let tok = self.consume(TokenType::Savant)?;
8271        let name = self.consume(TokenType::Identifier)?.value;
8272        let mut node = SavantDefinition {
8273            name,
8274            loc: Loc {
8275                line: tok.line,
8276                column: tok.column,
8277            },
8278            ..Default::default()
8279        };
8280        self.consume(TokenType::LBrace)?;
8281        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8282            let field = self.current().clone();
8283            let field_name = field.value.clone();
8284            self.advance();
8285            if self.check(TokenType::Colon) {
8286                self.advance();
8287                match field_name.as_str() {
8288                    "domain" => node.domain = self.consume(TokenType::StringLit)?.value,
8289                    other => {
8290                        return Err(self.error(&format!(
8291                            "unknown savant field `{other}` in savant `{}` — expected \
8292                             `domain:` or a `cognition` / `memory` / `budget` / `mandate` block",
8293                            node.name
8294                        )))
8295                    }
8296                }
8297            } else if field_name == "cognition" {
8298                node.cognition = Some(self.parse_savant_cognition(field.line, field.column)?);
8299            } else if field_name == "memory" {
8300                node.memory = Some(self.parse_savant_memory(field.line, field.column)?);
8301            } else if field_name == "budget" {
8302                node.budget = Some(self.parse_savant_budget(field.line, field.column)?);
8303            } else if field_name == "mandate" {
8304                node.mandates
8305                    .push(self.parse_savant_mandate(field.line, field.column)?);
8306            } else {
8307                return Err(self.error(&format!(
8308                    "unexpected `{field_name}` in savant `{}` body — expected `domain:` or a \
8309                     `cognition` / `memory` / `budget` / `mandate` block",
8310                    node.name
8311                )));
8312            }
8313            if self.check(TokenType::Comma) {
8314                self.advance();
8315            }
8316        }
8317        self.consume(TokenType::RBrace)?;
8318        Ok(node)
8319    }
8320
8321    /// §Fase 87.a — the `cognition { depth:, entropic_threshold:, divergence: }`
8322    /// sub-block. Catalog validation of `depth`/`divergence` is §87.b.
8323    fn parse_savant_cognition(
8324        &mut self,
8325        line: u32,
8326        column: u32,
8327    ) -> Result<SavantCognition, ParseError> {
8328        self.consume(TokenType::LBrace)?;
8329        let mut node = SavantCognition {
8330            loc: Loc { line, column },
8331            ..Default::default()
8332        };
8333        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8334            let key = self.consume_any_ident_or_kw()?.value;
8335            self.consume(TokenType::Colon)?;
8336            match key.as_str() {
8337                "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
8338                "entropic_threshold" => node.entropic_threshold = self.parse_optional_float(),
8339                "divergence" => node.divergence = self.consume_any_ident_or_kw()?.value,
8340                other => {
8341                    return Err(self.error(&format!(
8342                        "unknown savant `cognition` field `{other}` — expected \
8343                         `depth` / `entropic_threshold` / `divergence`"
8344                    )))
8345                }
8346            }
8347            if self.check(TokenType::Comma) {
8348                self.advance();
8349            }
8350        }
8351        self.consume(TokenType::RBrace)?;
8352        Ok(node)
8353    }
8354
8355    /// §Fase 87.a — the `memory { backend:, corpus_graph:, isolation_level: }`
8356    /// sub-block. `backend` is resolved to a declared `memory`/`corpus` in §87.c.
8357    fn parse_savant_memory(
8358        &mut self,
8359        line: u32,
8360        column: u32,
8361    ) -> Result<SavantMemory, ParseError> {
8362        self.consume(TokenType::LBrace)?;
8363        let mut node = SavantMemory {
8364            loc: Loc { line, column },
8365            ..Default::default()
8366        };
8367        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8368            let key = self.consume_any_ident_or_kw()?.value;
8369            self.consume(TokenType::Colon)?;
8370            match key.as_str() {
8371                "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
8372                "corpus_graph" => {
8373                    node.corpus_graph = self.consume_any_ident_or_kw()?.value == "true"
8374                }
8375                "isolation_level" => node.isolation_level = self.consume_any_ident_or_kw()?.value,
8376                other => {
8377                    return Err(self.error(&format!(
8378                        "unknown savant `memory` field `{other}` — expected \
8379                         `backend` / `corpus_graph` / `isolation_level`"
8380                    )))
8381                }
8382            }
8383            if self.check(TokenType::Comma) {
8384                self.advance();
8385            }
8386        }
8387        self.consume(TokenType::RBrace)?;
8388        Ok(node)
8389    }
8390
8391    /// §Fase 87.a — the `budget { max_iterations:, max_tool_synth: }` sub-block.
8392    /// Bound to a §72 linear budget (`RateLease`) in §87.c.
8393    fn parse_savant_budget(
8394        &mut self,
8395        line: u32,
8396        column: u32,
8397    ) -> Result<SavantBudget, ParseError> {
8398        self.consume(TokenType::LBrace)?;
8399        let mut node = SavantBudget {
8400            loc: Loc { line, column },
8401            ..Default::default()
8402        };
8403        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8404            let key = self.consume_any_ident_or_kw()?.value;
8405            self.consume(TokenType::Colon)?;
8406            match key.as_str() {
8407                "max_iterations" => node.max_iterations = self.parse_optional_int(),
8408                "max_tool_synth" => node.max_tool_synth = self.parse_optional_int(),
8409                other => {
8410                    return Err(self.error(&format!(
8411                        "unknown savant `budget` field `{other}` — expected \
8412                         `max_iterations` / `max_tool_synth`"
8413                    )))
8414                }
8415            }
8416            if self.check(TokenType::Comma) {
8417                self.advance();
8418            }
8419        }
8420        self.consume(TokenType::RBrace)?;
8421        Ok(node)
8422    }
8423
8424    /// §Fase 87.a — the `mandate <Name> { objective:, output: }` sub-block. The
8425    /// `mandate` keyword is already consumed by `parse_savant`.
8426    fn parse_savant_mandate(
8427        &mut self,
8428        line: u32,
8429        column: u32,
8430    ) -> Result<SavantMandate, ParseError> {
8431        let name = self.consume(TokenType::Identifier)?.value;
8432        let mut node = SavantMandate {
8433            name,
8434            loc: Loc { line, column },
8435            ..Default::default()
8436        };
8437        self.consume(TokenType::LBrace)?;
8438        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8439            let key = self.consume_any_ident_or_kw()?.value;
8440            self.consume(TokenType::Colon)?;
8441            match key.as_str() {
8442                "objective" => node.objective = self.consume(TokenType::StringLit)?.value,
8443                "output" => node.output_type = self.consume_any_ident_or_kw()?.value,
8444                other => {
8445                    return Err(self.error(&format!(
8446                        "unknown savant `mandate` field `{other}` — expected `objective` / `output`"
8447                    )))
8448                }
8449            }
8450            if self.check(TokenType::Comma) {
8451                self.advance();
8452            }
8453        }
8454        self.consume(TokenType::RBrace)?;
8455        Ok(node)
8456    }
8457
8458    /// §Fase 87.d — parse `synth <Name> { target:, risk:, language:, sandbox:,
8459    /// review:, max_lines: }`. Flat key:value block (the `cache` shape). Catalog
8460    /// + deny-by-default validation is §87.d `check_synth`. Unknown fields are a
8461    /// hard error (D83.7): a synth policy governs arbitrary-code execution.
8462    fn parse_synth(&mut self) -> Result<SynthDefinition, ParseError> {
8463        let tok = self.consume(TokenType::Synth)?;
8464        let name = self.consume(TokenType::Identifier)?.value;
8465        let mut node = SynthDefinition {
8466            name,
8467            loc: Loc {
8468                line: tok.line,
8469                column: tok.column,
8470            },
8471            ..Default::default()
8472        };
8473        self.consume(TokenType::LBrace)?;
8474        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8475            let key = self.consume_any_ident_or_kw()?.value;
8476            self.consume(TokenType::Colon)?;
8477            match key.as_str() {
8478                "target" => node.target = self.consume(TokenType::StringLit)?.value,
8479                "risk" => node.risk = self.consume_any_ident_or_kw()?.value,
8480                "language" => node.language = self.consume_any_ident_or_kw()?.value,
8481                "sandbox" => node.sandbox = self.consume_any_ident_or_kw()?.value,
8482                "review" => node.review = self.consume_any_ident_or_kw()?.value,
8483                "max_lines" => node.max_lines = self.parse_optional_int(),
8484                other => {
8485                    return Err(self.error(&format!(
8486                        "unknown synth field `{other}` in synth `{}` — expected `target` / `risk` \
8487                         / `language` / `sandbox` / `review` / `max_lines`",
8488                        node.name
8489                    )))
8490                }
8491            }
8492            if self.check(TokenType::Comma) {
8493                self.consume(TokenType::Comma)?;
8494            }
8495        }
8496        self.consume(TokenType::RBrace)?;
8497        Ok(node)
8498    }
8499
8500    /// §Fase 80.g — parse `voice Name { fields }`. Cross-field laws
8501    /// (stt/tts XOR realtime, interruptible ⇒ legal_basis, ref resolution)
8502    /// are §80.c type-checker territory (T852), same parse/check split as
8503    /// every primitive in this file.
8504    fn parse_voice(&mut self) -> Result<VoiceDefinition, ParseError> {
8505        let tok = self.consume(TokenType::Voice)?;
8506        let name = self.consume(TokenType::Identifier)?.value;
8507        let mut node = VoiceDefinition {
8508            name,
8509            loc: Loc { line: tok.line, column: tok.column },
8510            ..Default::default()
8511        };
8512        self.consume(TokenType::LBrace)?;
8513        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8514            let key = self.consume_any_ident_or_kw()?.value;
8515            self.consume(TokenType::Colon)?;
8516            match key.as_str() {
8517                // Each leg: a declared upstream name or a `Preset@vN` ref.
8518                "stt" => node.stt = Some(self.parse_upstream_ref()?),
8519                "tts" => node.tts = Some(self.parse_upstream_ref()?),
8520                "realtime" => node.realtime = Some(self.parse_upstream_ref()?),
8521                "carrier" => node.carrier = self.consume_any_ident_or_kw()?.value,
8522                "interruptible" => {
8523                    let v = self.consume_any_ident_or_kw()?.value;
8524                    node.interruptible = v == "true";
8525                }
8526                "legal_basis" => node.legal_basis = Some(self.consume_any_ident_or_kw()?.value),
8527                "persona" => node.persona = Some(self.consume(TokenType::Identifier)?.value),
8528                "context" => node.context = Some(self.consume(TokenType::Identifier)?.value),
8529                other => return Err(self.error(&format!("unknown voice field `{other}`"))),
8530            }
8531            if self.check(TokenType::Comma) {
8532                self.consume(TokenType::Comma)?;
8533            }
8534        }
8535        self.consume(TokenType::RBrace)?;
8536        Ok(node)
8537    }
8538
8539    /// §Fase 80.g — an upstream leg reference: `Ident` (a declared
8540    /// `upstream`) or `Ident@vN` (a §80.f preset).
8541    fn parse_upstream_ref(&mut self) -> Result<String, ParseError> {
8542        let base = self.consume(TokenType::Identifier)?.value;
8543        if self.check(TokenType::At) {
8544            self.advance();
8545            let version = self.consume_any_ident_or_kw()?.value;
8546            Ok(format!("{base}@{version}"))
8547        } else {
8548            Ok(base)
8549        }
8550    }
8551
8552    /// §Fase 80.b — parse the `map: [ rule, … ]` projection list.
8553    ///
8554    /// rule := (`send` | `receive`) <MessageType> `as` (`json` | `binary`)
8555    ///         [ `tag` <string> ]                 — send-json only
8556    ///         [ `when` <string> `=` <string> ]   — receive-json only
8557    fn parse_upstream_map(&mut self) -> Result<Vec<UpstreamMapRule>, ParseError> {
8558        self.consume(TokenType::LBracket)?;
8559        let mut rules = Vec::new();
8560        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
8561            let dir_tok = self.current().clone();
8562            let direction = match dir_tok.ttype {
8563                TokenType::Send => "send",
8564                TokenType::Receive => "receive",
8565                _ => {
8566                    return Err(self.error(&format!(
8567                        "upstream map rule must start with `send` or `receive`, got `{}`",
8568                        dir_tok.value
8569                    )))
8570                }
8571            };
8572            self.advance();
8573            let message = self.consume(TokenType::Identifier)?.value;
8574            self.consume(TokenType::As)?;
8575            let framing = self.consume_any_ident_or_kw()?.value;
8576            let mut rule = UpstreamMapRule {
8577                direction: direction.to_string(),
8578                message,
8579                framing,
8580                loc: Loc { line: dir_tok.line, column: dir_tok.column },
8581                ..Default::default()
8582            };
8583            // Optional selectors — contextual identifiers, not keywords.
8584            if self.current().value == "tag" {
8585                self.advance();
8586                rule.tag = Some(self.consume(TokenType::StringLit)?.value);
8587            } else if self.current().value == "when" {
8588                // `when "f" = "v"` — equality discriminator; `when "f"` —
8589                // field-PRESENCE discriminator (vendors like Gemini Live /
8590                // ElevenLabs mark frame kinds by which key exists, not by a
8591                // type value).
8592                self.advance();
8593                rule.when_field = Some(self.consume(TokenType::StringLit)?.value);
8594                if self.check(TokenType::Assign) {
8595                    self.advance();
8596                    rule.when_value = Some(self.consume(TokenType::StringLit)?.value);
8597                }
8598            }
8599            rules.push(rule);
8600            if self.check(TokenType::Comma) {
8601                self.advance();
8602            }
8603        }
8604        self.consume(TokenType::RBracket)?;
8605        Ok(rules)
8606    }
8607
8608    /// §Fase 80.b — parse `reconnect: { backoff_ms: <int>, max_attempts:
8609    /// <int>, on_exhausted: <ident> }` (order-free, all three required —
8610    /// a reconnection policy with a hole is not a policy).
8611    fn parse_upstream_reconnect(&mut self) -> Result<UpstreamReconnect, ParseError> {
8612        self.consume(TokenType::LBrace)?;
8613        let mut backoff_ms: Option<i64> = None;
8614        let mut max_attempts: Option<i64> = None;
8615        let mut on_exhausted: Option<String> = None;
8616        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8617            let key = self.consume_any_ident_or_kw()?.value;
8618            self.consume(TokenType::Colon)?;
8619            match key.as_str() {
8620                "backoff_ms" => {
8621                    backoff_ms = Some(
8622                        self.consume(TokenType::Integer)?
8623                            .value
8624                            .parse::<i64>()
8625                            .map_err(|_| self.error("backoff_ms must be an integer"))?,
8626                    )
8627                }
8628                "max_attempts" => {
8629                    max_attempts = Some(
8630                        self.consume(TokenType::Integer)?
8631                            .value
8632                            .parse::<i64>()
8633                            .map_err(|_| self.error("max_attempts must be an integer"))?,
8634                    )
8635                }
8636                "on_exhausted" => on_exhausted = Some(self.consume_any_ident_or_kw()?.value),
8637                other => return Err(self.error(&format!("unknown reconnect field `{other}`"))),
8638            }
8639            if self.check(TokenType::Comma) {
8640                self.consume(TokenType::Comma)?;
8641            }
8642        }
8643        self.consume(TokenType::RBrace)?;
8644        match (backoff_ms, max_attempts, on_exhausted) {
8645            (Some(b), Some(m), Some(o)) => Ok(UpstreamReconnect { backoff_ms: b, max_attempts: m, on_exhausted: o }),
8646            _ => Err(self.error(
8647                "reconnect requires all of `backoff_ms:`, `max_attempts:`, `on_exhausted:` — a reconnection policy with a hole is not a policy",
8648            )),
8649        }
8650    }
8651
8652    /// Parse: `[send T, receive U, loop, end]`.
8653    fn parse_session_steps(&mut self) -> Result<Vec<SessionStep>, ParseError> {
8654        self.consume(TokenType::LBracket)?;
8655        let mut steps = Vec::new();
8656        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
8657            steps.push(self.parse_session_step()?);
8658            if self.check(TokenType::Comma) {
8659                self.advance();
8660            }
8661        }
8662        self.consume(TokenType::RBracket)?;
8663        Ok(steps)
8664    }
8665
8666    /// §Fase 79.b — a **brace**-delimited session step block: `{ step, step, … }`.
8667    /// Used by the `interrupt`/`resumable` regions (the paper's block surface),
8668    /// as opposed to the `[ … ]` step-lists used by roles and choice arms.
8669    fn parse_session_step_block(&mut self) -> Result<Vec<SessionStep>, ParseError> {
8670        self.consume(TokenType::LBrace)?;
8671        let mut steps = Vec::new();
8672        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8673            steps.push(self.parse_session_step()?);
8674            if self.check(TokenType::Comma) {
8675                self.advance();
8676            }
8677        }
8678        self.consume(TokenType::RBrace)?;
8679        Ok(steps)
8680    }
8681
8682    fn parse_session_step(&mut self) -> Result<SessionStep, ParseError> {
8683        let tok = self.current().clone();
8684        let loc = Loc { line: tok.line, column: tok.column };
8685        match tok.ttype {
8686            TokenType::Send => {
8687                self.advance();
8688                let msg = self.consume_any_ident_or_kw()?;
8689                Ok(SessionStep { op: "send".into(), message_type: msg.value, loc, ..Default::default() })
8690            }
8691            TokenType::Receive => {
8692                self.advance();
8693                let msg = self.consume_any_ident_or_kw()?;
8694                Ok(SessionStep { op: "receive".into(), message_type: msg.value, loc, ..Default::default() })
8695            }
8696            TokenType::Loop => {
8697                self.advance();
8698                Ok(SessionStep { op: "loop".into(), loc, ..Default::default() })
8699            }
8700            TokenType::End => {
8701                self.advance();
8702                Ok(SessionStep { op: "end".into(), loc, ..Default::default() })
8703            }
8704            // §Fase 41.b — choice: `select { ℓ: [..], … }` (⊕) | `branch { ℓ: [..], … }` (&).
8705            // `select`/`branch` are not keywords — they arrive as identifiers.
8706            TokenType::Identifier if tok.value == "select" || tok.value == "branch" => {
8707                self.parse_session_choice(&tok.value, loc)
8708            }
8709            // §Fase 79.b — `interrupt { <body> } on <Signal> as <sig> resumable { <handler> }`.
8710            // Contextual keyword (identifier), like `select`/`branch`.
8711            TokenType::Identifier if tok.value == "interrupt" => {
8712                self.parse_session_interrupt(loc)
8713            }
8714            // §Fase 79.b — `resume`: the handler's normal exit (hand control back to
8715            // the parked body). A bare step, no payload; only meaningful inside an
8716            // `interrupt` handler (enforced at type-check, §79.c).
8717            TokenType::Identifier if tok.value == "resume" => {
8718                self.advance();
8719                Ok(SessionStep { op: "resume".into(), loc, ..Default::default() })
8720            }
8721            _ => Err(ParseError {
8722                message: format!(
8723                    "Invalid session step '{}' — expected send | receive | loop | end | select | branch | interrupt | resume",
8724                    tok.value
8725                ),
8726                line: tok.line,
8727                column: tok.column,
8728                ..Default::default()
8729            }),
8730        }
8731    }
8732
8733    /// §Fase 79.b — consume a **contextual keyword** (`on` / `as` / `resumable`):
8734    /// a token whose *value* must equal `kw`, regardless of whether the lexer
8735    /// classified it as a keyword or a bare identifier. Keeps the `interrupt`
8736    /// surface readable without minting three reserved words.
8737    fn consume_contextual(&mut self, kw: &str) -> Result<(), ParseError> {
8738        let t = self.current().clone();
8739        if t.value != kw {
8740            return Err(ParseError {
8741                message: format!("expected `{kw}` in interrupt step, got `{}`", t.value),
8742                line: t.line,
8743                column: t.column,
8744                ..Default::default()
8745            });
8746        }
8747        self.advance();
8748        Ok(())
8749    }
8750
8751    /// §Fase 79.b — Parse an interruptible region:
8752    /// `interrupt { <body-steps> } on <Signal> as <sig> resumable { <handler-steps> }`.
8753    ///
8754    /// Encoded into the string-tagged `SessionStep` (mirroring the §41.b choice
8755    /// shape): `op = "interrupt"`, `message_type = <Signal>` (validated against the
8756    /// closed `CallInterruptCause` catalog at type-check, §79.c), two labelled
8757    /// `branches` (`body`, `handler`), `binder = <sig>`, `resumable = true`.
8758    fn parse_session_interrupt(&mut self, loc: Loc) -> Result<SessionStep, ParseError> {
8759        self.advance(); // consume `interrupt`
8760        // Body region — a brace-delimited step block (the paper's `interrupt { … }`
8761        // surface; distinct from the `[ … ]` step-lists of roles/choice arms).
8762        let body = self.parse_session_step_block()?;
8763        // `on <Signal>`
8764        self.consume_contextual("on")?;
8765        let signal = self.consume_any_ident_or_kw()?;
8766        // `as <sig>`
8767        self.consume_contextual("as")?;
8768        let binder = self.consume_any_ident_or_kw()?;
8769        // `resumable { <handler> }`
8770        self.consume_contextual("resumable")?;
8771        let handler = self.parse_session_step_block()?;
8772        Ok(SessionStep {
8773            op: "interrupt".into(),
8774            message_type: signal.value,
8775            branches: vec![
8776                SessionBranch { label: "body".into(), steps: body, loc: loc.clone() },
8777                SessionBranch { label: "handler".into(), steps: handler, loc: loc.clone() },
8778            ],
8779            binder: binder.value,
8780            resumable: true,
8781            loc,
8782        })
8783    }
8784
8785    /// §Fase 41.b — Parse a choice step: `select { ask: [..], cancel: [..] }`
8786    /// (or `branch { … }`). Each `label: [steps]` arm is a nested sub-protocol.
8787    fn parse_session_choice(&mut self, op: &str, loc: Loc) -> Result<SessionStep, ParseError> {
8788        self.advance(); // consume `select` / `branch`
8789        self.consume(TokenType::LBrace)?;
8790        let mut branches = Vec::new();
8791        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8792            let label_tok = self.consume_any_ident_or_kw()?;
8793            self.consume(TokenType::Colon)?;
8794            let steps = self.parse_session_steps()?;
8795            branches.push(SessionBranch {
8796                label: label_tok.value,
8797                steps,
8798                loc: Loc { line: label_tok.line, column: label_tok.column },
8799            });
8800            if self.check(TokenType::Comma) {
8801                self.advance();
8802            }
8803        }
8804        self.consume(TokenType::RBrace)?;
8805        Ok(SessionStep { op: op.to_string(), branches, loc, ..Default::default() })
8806    }
8807
8808    /// Parse: `topology Name { nodes: [A, B, …]  edges: [A -> B : Session, …] }`.
8809    fn parse_topology(&mut self) -> Result<TopologyDefinition, ParseError> {
8810        let tok = self.consume(TokenType::Topology)?;
8811        let name = self.consume(TokenType::Identifier)?.value;
8812        let mut node = TopologyDefinition {
8813            name,
8814            nodes: Vec::new(),
8815            edges: Vec::new(),
8816            loc: Loc {
8817                line: tok.line,
8818                column: tok.column,
8819            },
8820            leading_trivia: Vec::new(),
8821            trailing_trivia: Vec::new(),
8822        };
8823        self.consume(TokenType::LBrace)?;
8824        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8825            let field_name = self.current().value.clone();
8826            self.advance();
8827            if !self.check(TokenType::Colon) {
8828                if self.check(TokenType::LBrace) {
8829                    self.skip_braced_block()?;
8830                }
8831                continue;
8832            }
8833            self.advance();
8834            match field_name.as_str() {
8835                "nodes" => node.nodes = self.parse_bracketed_identifiers()?,
8836                "edges" => node.edges = self.parse_topology_edges()?,
8837                _ => self.skip_value(),
8838            }
8839        }
8840        self.consume(TokenType::RBrace)?;
8841        Ok(node)
8842    }
8843
8844    fn parse_topology_edges(&mut self) -> Result<Vec<TopologyEdge>, ParseError> {
8845        self.consume(TokenType::LBracket)?;
8846        let mut edges = Vec::new();
8847        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
8848            edges.push(self.parse_topology_edge()?);
8849            if self.check(TokenType::Comma) {
8850                self.advance();
8851            }
8852        }
8853        self.consume(TokenType::RBracket)?;
8854        Ok(edges)
8855    }
8856
8857    fn parse_topology_edge(&mut self) -> Result<TopologyEdge, ParseError> {
8858        let src_tok = self.consume_any_ident_or_kw()?;
8859        self.consume(TokenType::Arrow)?;
8860        let tgt_tok = self.consume_any_ident_or_kw()?;
8861        self.consume(TokenType::Colon)?;
8862        let sess_tok = self.consume_any_ident_or_kw()?;
8863        Ok(TopologyEdge {
8864            source: src_tok.value,
8865            target: tgt_tok.value,
8866            session_ref: sess_tok.value,
8867            loc: Loc {
8868                line: src_tok.line,
8869                column: src_tok.column,
8870            },
8871        })
8872    }
8873
8874    // ── §λ-L-E Fase 5 — Cognitive immune system (paper_immune_v2.md) ────
8875
8876    /// Parse: `immune Name { watch, sensitivity, baseline, window, scope, tau, decay }`.
8877    fn parse_immune(&mut self) -> Result<ImmuneDefinition, ParseError> {
8878        let tok = self.consume(TokenType::Immune)?;
8879        let name = self.consume(TokenType::Identifier)?.value;
8880        let mut node = ImmuneDefinition {
8881            name,
8882            watch: Vec::new(),
8883            sensitivity: None,
8884            baseline: "learned".to_string(),
8885            window: 100,
8886            scope: String::new(),
8887            tau: String::new(),
8888            decay: "exponential".to_string(),
8889            loc: Loc {
8890                line: tok.line,
8891                column: tok.column,
8892            },
8893            leading_trivia: Vec::new(),
8894            trailing_trivia: Vec::new(),
8895        };
8896        self.consume(TokenType::LBrace)?;
8897        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8898            let field_name = self.current().value.clone();
8899            self.advance();
8900            if !self.check(TokenType::Colon) {
8901                if self.check(TokenType::LBrace) {
8902                    self.skip_braced_block()?;
8903                }
8904                continue;
8905            }
8906            self.advance();
8907            match field_name.as_str() {
8908                "watch" => node.watch = self.parse_bracketed_identifiers()?,
8909                "sensitivity" => node.sensitivity = self.parse_optional_float(),
8910                "baseline" => node.baseline = self.consume_any_ident_or_kw()?.value,
8911                "window" => {
8912                    if let Some(v) = self.parse_optional_int() {
8913                        node.window = v;
8914                    }
8915                }
8916                "scope" => {
8917                    let s_tok = self.consume_any_ident_or_kw()?;
8918                    let s = s_tok.value;
8919                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
8920                        return Err(ParseError {
8921                            message: format!(
8922                                "Invalid scope '{s}' in immune '{}' — \
8923                                 expected tenant | flow | global",
8924                                node.name
8925                            ),
8926                            line: s_tok.line,
8927                            column: s_tok.column,
8928                                                    ..Default::default()
8929                        });
8930                    }
8931                    node.scope = s;
8932                }
8933                "tau" => {
8934                    let t = self.current().clone();
8935                    match t.ttype {
8936                        TokenType::Duration | TokenType::StringLit => {
8937                            self.advance();
8938                            node.tau = t.value;
8939                        }
8940                        _ => node.tau = self.consume_any_ident_or_kw()?.value,
8941                    }
8942                }
8943                "decay" => {
8944                    let d_tok = self.consume_any_ident_or_kw()?;
8945                    let d = d_tok.value;
8946                    if !matches!(d.as_str(), "exponential" | "linear" | "none") {
8947                        return Err(ParseError {
8948                            message: format!(
8949                                "Invalid decay '{d}' in immune '{}' — \
8950                                 expected exponential | linear | none",
8951                                node.name
8952                            ),
8953                            line: d_tok.line,
8954                            column: d_tok.column,
8955                                                    ..Default::default()
8956                        });
8957                    }
8958                    node.decay = d;
8959                }
8960                _ => self.skip_value(),
8961            }
8962        }
8963        self.consume(TokenType::RBrace)?;
8964        Ok(node)
8965    }
8966
8967    /// Parse: `reflex Name { trigger, on_level, action, scope, sla }`.
8968    fn parse_reflex(&mut self) -> Result<ReflexDefinition, ParseError> {
8969        let tok = self.consume(TokenType::Reflex)?;
8970        let name = self.consume(TokenType::Identifier)?.value;
8971        let mut node = ReflexDefinition {
8972            name,
8973            trigger: String::new(),
8974            on_level: "doubt".to_string(),
8975            action: String::new(),
8976            scope: String::new(),
8977            sla: String::new(),
8978            loc: Loc {
8979                line: tok.line,
8980                column: tok.column,
8981            },
8982            leading_trivia: Vec::new(),
8983            trailing_trivia: Vec::new(),
8984        };
8985        self.consume(TokenType::LBrace)?;
8986        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8987            let field_name = self.current().value.clone();
8988            self.advance();
8989            if !self.check(TokenType::Colon) {
8990                if self.check(TokenType::LBrace) {
8991                    self.skip_braced_block()?;
8992                }
8993                continue;
8994            }
8995            self.advance();
8996            match field_name.as_str() {
8997                "trigger" => node.trigger = self.consume_any_ident_or_kw()?.value,
8998                "on_level" => {
8999                    let l_tok = self.consume_any_ident_or_kw()?;
9000                    let l = l_tok.value;
9001                    if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
9002                        return Err(ParseError {
9003                            message: format!(
9004                                "Invalid on_level '{l}' in reflex '{}' — \
9005                                 expected know | believe | speculate | doubt",
9006                                node.name
9007                            ),
9008                            line: l_tok.line,
9009                            column: l_tok.column,
9010                                                    ..Default::default()
9011                        });
9012                    }
9013                    node.on_level = l;
9014                }
9015                "action" => {
9016                    let a_tok = self.consume_any_ident_or_kw()?;
9017                    let a = a_tok.value;
9018                    if !matches!(
9019                        a.as_str(),
9020                        "drop"
9021                            | "revoke"
9022                            | "emit"
9023                            | "redact"
9024                            | "quarantine"
9025                            | "terminate"
9026                            | "alert"
9027                    ) {
9028                        return Err(ParseError {
9029                            message: format!(
9030                                "Invalid action '{a}' in reflex '{}' — \
9031                                 expected drop | revoke | emit | redact | \
9032                                 quarantine | terminate | alert",
9033                                node.name
9034                            ),
9035                            line: a_tok.line,
9036                            column: a_tok.column,
9037                                                    ..Default::default()
9038                        });
9039                    }
9040                    node.action = a;
9041                }
9042                "scope" => {
9043                    let s_tok = self.consume_any_ident_or_kw()?;
9044                    let s = s_tok.value;
9045                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
9046                        return Err(ParseError {
9047                            message: format!(
9048                                "Invalid scope '{s}' in reflex '{}' — \
9049                                 expected tenant | flow | global",
9050                                node.name
9051                            ),
9052                            line: s_tok.line,
9053                            column: s_tok.column,
9054                                                    ..Default::default()
9055                        });
9056                    }
9057                    node.scope = s;
9058                }
9059                "sla" => {
9060                    let t = self.current().clone();
9061                    match t.ttype {
9062                        TokenType::Duration | TokenType::StringLit => {
9063                            self.advance();
9064                            node.sla = t.value;
9065                        }
9066                        _ => node.sla = self.consume_any_ident_or_kw()?.value,
9067                    }
9068                }
9069                _ => self.skip_value(),
9070            }
9071        }
9072        self.consume(TokenType::RBrace)?;
9073        Ok(node)
9074    }
9075
9076    /// Parse: `heal Name { source, on_level, mode, scope, review_sla, shield, max_patches }`.
9077    fn parse_heal(&mut self) -> Result<HealDefinition, ParseError> {
9078        let tok = self.consume(TokenType::Heal)?;
9079        let name = self.consume(TokenType::Identifier)?.value;
9080        let mut node = HealDefinition {
9081            name,
9082            source: String::new(),
9083            on_level: "doubt".to_string(),
9084            mode: "human_in_loop".to_string(),
9085            scope: String::new(),
9086            review_sla: String::new(),
9087            shield_ref: String::new(),
9088            max_patches: 3,
9089            loc: Loc {
9090                line: tok.line,
9091                column: tok.column,
9092            },
9093            leading_trivia: Vec::new(),
9094            trailing_trivia: Vec::new(),
9095        };
9096        self.consume(TokenType::LBrace)?;
9097        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9098            let field_name = self.current().value.clone();
9099            self.advance();
9100            if !self.check(TokenType::Colon) {
9101                if self.check(TokenType::LBrace) {
9102                    self.skip_braced_block()?;
9103                }
9104                continue;
9105            }
9106            self.advance();
9107            match field_name.as_str() {
9108                "source" => node.source = self.consume_any_ident_or_kw()?.value,
9109                "on_level" => {
9110                    let l_tok = self.consume_any_ident_or_kw()?;
9111                    let l = l_tok.value;
9112                    if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
9113                        return Err(ParseError {
9114                            message: format!(
9115                                "Invalid on_level '{l}' in heal '{}' — \
9116                                 expected know | believe | speculate | doubt",
9117                                node.name
9118                            ),
9119                            line: l_tok.line,
9120                            column: l_tok.column,
9121                                                    ..Default::default()
9122                        });
9123                    }
9124                    node.on_level = l;
9125                }
9126                "mode" => {
9127                    let m_tok = self.consume_any_ident_or_kw()?;
9128                    let m = m_tok.value;
9129                    if !matches!(m.as_str(), "audit_only" | "human_in_loop" | "adversarial") {
9130                        return Err(ParseError {
9131                            message: format!(
9132                                "Invalid mode '{m}' in heal '{}' — \
9133                                 expected audit_only | human_in_loop | adversarial",
9134                                node.name
9135                            ),
9136                            line: m_tok.line,
9137                            column: m_tok.column,
9138                                                    ..Default::default()
9139                        });
9140                    }
9141                    node.mode = m;
9142                }
9143                "scope" => {
9144                    let s_tok = self.consume_any_ident_or_kw()?;
9145                    let s = s_tok.value;
9146                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
9147                        return Err(ParseError {
9148                            message: format!(
9149                                "Invalid scope '{s}' in heal '{}' — \
9150                                 expected tenant | flow | global",
9151                                node.name
9152                            ),
9153                            line: s_tok.line,
9154                            column: s_tok.column,
9155                                                    ..Default::default()
9156                        });
9157                    }
9158                    node.scope = s;
9159                }
9160                "review_sla" => {
9161                    let t = self.current().clone();
9162                    match t.ttype {
9163                        TokenType::Duration | TokenType::StringLit => {
9164                            self.advance();
9165                            node.review_sla = t.value;
9166                        }
9167                        _ => node.review_sla = self.consume_any_ident_or_kw()?.value,
9168                    }
9169                }
9170                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
9171                "max_patches" => {
9172                    if let Some(v) = self.parse_optional_int() {
9173                        node.max_patches = v;
9174                    }
9175                }
9176                _ => self.skip_value(),
9177            }
9178        }
9179        self.consume(TokenType::RBrace)?;
9180        Ok(node)
9181    }
9182
9183    // ── §λ-L-E Fase 9 — UI cognitiva (component / view) ────────────
9184
9185    /// Parse: `component Name { renders, via_shield, on_interact, render_hint }`.
9186    fn parse_component(&mut self) -> Result<ComponentDefinition, ParseError> {
9187        let tok = self.consume(TokenType::Component)?;
9188        let name = self.consume(TokenType::Identifier)?.value;
9189        let mut node = ComponentDefinition {
9190            name,
9191            renders: String::new(),
9192            via_shield: String::new(),
9193            on_interact: String::new(),
9194            render_hint: "custom".to_string(),
9195            loc: Loc {
9196                line: tok.line,
9197                column: tok.column,
9198            },
9199            leading_trivia: Vec::new(),
9200            trailing_trivia: Vec::new(),
9201        };
9202        self.consume(TokenType::LBrace)?;
9203        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9204            let field_name = self.current().value.clone();
9205            self.advance();
9206            if !self.check(TokenType::Colon) {
9207                if self.check(TokenType::LBrace) {
9208                    self.skip_braced_block()?;
9209                }
9210                continue;
9211            }
9212            self.advance();
9213            match field_name.as_str() {
9214                "renders" => node.renders = self.consume_any_ident_or_kw()?.value,
9215                "via_shield" => node.via_shield = self.consume_any_ident_or_kw()?.value,
9216                "on_interact" => node.on_interact = self.consume_any_ident_or_kw()?.value,
9217                "render_hint" => {
9218                    let h_tok = self.consume_any_ident_or_kw()?;
9219                    let h = h_tok.value;
9220                    if !matches!(h.as_str(), "card" | "list" | "form" | "chart" | "custom") {
9221                        return Err(ParseError {
9222                            message: format!(
9223                                "Invalid render_hint '{h}' in component '{}' — \
9224                                 expected card | list | form | chart | custom",
9225                                node.name
9226                            ),
9227                            line: h_tok.line,
9228                            column: h_tok.column,
9229                                                    ..Default::default()
9230                        });
9231                    }
9232                    node.render_hint = h;
9233                }
9234                _ => self.skip_value(),
9235            }
9236        }
9237        self.consume(TokenType::RBrace)?;
9238        Ok(node)
9239    }
9240
9241    /// Parse: `view Name { title, components: [...], route }`.
9242    fn parse_view(&mut self) -> Result<ViewDefinition, ParseError> {
9243        let tok = self.consume(TokenType::View)?;
9244        let name = self.consume(TokenType::Identifier)?.value;
9245        let mut node = ViewDefinition {
9246            name,
9247            title: String::new(),
9248            components: Vec::new(),
9249            route: String::new(),
9250            loc: Loc {
9251                line: tok.line,
9252                column: tok.column,
9253            },
9254            leading_trivia: Vec::new(),
9255            trailing_trivia: Vec::new(),
9256        };
9257        self.consume(TokenType::LBrace)?;
9258        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9259            let field_name = self.current().value.clone();
9260            self.advance();
9261            if !self.check(TokenType::Colon) {
9262                if self.check(TokenType::LBrace) {
9263                    self.skip_braced_block()?;
9264                }
9265                continue;
9266            }
9267            self.advance();
9268            match field_name.as_str() {
9269                "title" => node.title = self.consume(TokenType::StringLit)?.value,
9270                "components" => node.components = self.parse_bracketed_identifiers()?,
9271                "route" => node.route = self.consume(TokenType::StringLit)?.value,
9272                _ => self.skip_value(),
9273            }
9274        }
9275        self.consume(TokenType::RBrace)?;
9276        Ok(node)
9277    }
9278
9279    fn parse_axonendpoint(&mut self) -> Result<AxonEndpointDefinition, ParseError> {
9280        let tok = self.consume(TokenType::AxonEndpoint)?;
9281        let name = self.consume(TokenType::Identifier)?.value;
9282        let mut node = AxonEndpointDefinition {
9283            name,
9284            method: String::new(),
9285            path: String::new(),
9286            body_type: String::new(),
9287            execute_flow: String::new(),
9288            output_type: String::new(),
9289            shield_ref: String::new(),
9290            // §Fase 83.a — `cors:` reference; empty ≡ no cors declared
9291            // (D83.5: no CORS headers, ever — secure by default).
9292            cors_ref: String::new(),
9293            retries: None,
9294            timeout: String::new(),
9295            compliance: Vec::new(),
9296            // §Fase 30 — Defaults preserve backwards compat per D1.
9297            transport: "json".to_string(),
9298            keepalive: String::new(),
9299            // §Fase 31.b — Inference fields (parser-default state).
9300            // Both fields toggle/populate only when the source provides
9301            // an explicit `transport:` declaration (parser sets
9302            // `transport_explicit = true`) AND the type-checker walks
9303            // the program to compute `implicit_transport`.
9304            transport_explicit: false,
9305            implicit_transport: String::new(),
9306            // §Fase 32.g (D8) — auth scope; empty list ≡ no auth gate.
9307            requires_capabilities: Vec::new(),
9308            // §Fase 89.a — explicit authorization-coverage opt-out. Default
9309            // false; the §89.b rule requires coverage OR `public: true`.
9310            public: false,
9311            // §Fase 32.h — Replay-token binding (D9 plan-vivo).
9312            // Parser defaults: not explicit; effective value resolved
9313            // at deploy time using the method-default heuristic.
9314            replay_explicit: false,
9315            replay: false,
9316            // §Fase 33.z.k.b (v1.28.0) — Wire-format dialect default
9317            // empty; the runtime classifier resolves the default
9318            // dialect per the algebraic-effect predicate when the
9319            // source omits `transport: sse(<dialect>)`.
9320            transport_dialect: String::new(),
9321            // §Fase 33.z.k.1 (v1.27.1) — Algebraic-effect override.
9322            // Parser default false; populated by the type-checker's
9323            // compute_implicit_transports pass once the full program
9324            // is known (the predicate cross-references tool effects
9325            // declared anywhere in the program).
9326            has_algebraic_stream_effect: false,
9327            // §Fase 36.d (D2) — declared execution backend; empty ≡
9328            // not declared (the endpoint resolves down the Fase 36 D1
9329            // ladder). A non-empty value is validated against the
9330            // closed `AXONENDPOINT_BACKEND_VALUES` catalog below.
9331            backend: String::new(),
9332            // §Fase 37.y (D1) — Path-param names extracted from the
9333            // `path:` string AFTER the field is parsed. Initialized
9334            // empty; populated by `extract_path_param_names` after
9335            // the `path:` field is read in the loop below.
9336            path_params: Vec::new(),
9337            // §Fase 37.y (D2) — Inline `query: { name: Type, name: Type? }`
9338            // block. Initialized empty; populated by the `"query"` arm
9339            // in the field loop below. Closed catalog enforced at parse
9340            // time per `axonendpoint_is_valid_query_param_type`.
9341            query_params: Vec::new(),
9342            loc: Loc {
9343                line: tok.line,
9344                column: tok.column,
9345            },
9346            leading_trivia: Vec::new(),
9347            trailing_trivia: Vec::new(),
9348        };
9349        self.consume(TokenType::LBrace)?;
9350        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9351            let field_name = self.current().value.clone();
9352            self.advance();
9353            if self.check(TokenType::Colon) {
9354                self.advance();
9355                match field_name.as_str() {
9356                    "method" => {
9357                        // §Fase 32.b D3 — closed method enum
9358                        // `{GET, POST, PUT, DELETE, PATCH}`. Unknown
9359                        // values rejected at parse time with smart-
9360                        // suggest hint (Fase 28.e). HEAD/OPTIONS/etc.
9361                        // are runtime-managed and not adopter-
9362                        // declarable.
9363                        let value_tok = self.consume_any_ident_or_kw()?;
9364                        let value_upper = value_tok.value.to_uppercase();
9365                        if !axonendpoint_is_valid_method(&value_upper) {
9366                            let hint = crate::smart_suggest::suggest_for(
9367                                &value_upper,
9368                                AXONENDPOINT_METHOD_VALUES,
9369                            );
9370                            let base = format!(
9371                                "Invalid method '{}' in axonendpoint '{}'.",
9372                                value_tok.value, node.name
9373                            );
9374                            let message = if hint.is_empty() {
9375                                format!(
9376                                    "{base} expected GET | POST | PUT | DELETE | PATCH, found {}",
9377                                    value_tok.value
9378                                )
9379                            } else {
9380                                format!(
9381                                    "{base} {hint} (expected GET | POST | PUT | DELETE | PATCH, found {})",
9382                                    value_tok.value
9383                                )
9384                            };
9385                            return Err(ParseError {
9386                                message,
9387                                line: value_tok.line,
9388                                column: value_tok.column,
9389                                ..Default::default()
9390                            });
9391                        }
9392                        node.method = value_upper;
9393                    }
9394                    "path" => {
9395                        node.path = self.consume(TokenType::StringLit)?.value.clone();
9396                        // §Fase 37.y (D1) — extract `{name}` placeholders
9397                        // for the Request Binding Contract's path-param
9398                        // source. Duplicate `{name}` in the same path
9399                        // is rejected at parse time (HTTP route patterns
9400                        // structurally reject duplicates; surfacing the
9401                        // error here is friendlier than letting axum
9402                        // panic at registration).
9403                        match extract_path_param_names(&node.path) {
9404                            Ok(names) => node.path_params = names,
9405                            Err(dup) => {
9406                                let cur = self.current().clone();
9407                                return Err(ParseError {
9408                                    message: format!(
9409                                        "axonendpoint '{}' declares path '{}' \
9410                                         containing duplicate placeholder '{{{}}}'. \
9411                                         Each `{{name}}` in a `path:` must be \
9412                                         unique — the runtime cannot bind two \
9413                                         path segments to the same name (Fase 37.y D1).",
9414                                        node.name, node.path, dup,
9415                                    ),
9416                                    line: cur.line,
9417                                    column: cur.column,
9418                                    ..Default::default()
9419                                });
9420                            }
9421                        }
9422                    },
9423                    "body" => node.body_type = self.consume_any_ident_or_kw()?.value.clone(),
9424                    "query" => {
9425                        // §Fase 37.y (D2) — Inline query-parameter block.
9426                        // Grammar: `query: { name: Type [, name: Type?]* }`.
9427                        // Closed type catalog
9428                        // `AXONENDPOINT_QUERY_PARAM_TYPES = {Text, Int,
9429                        // Float, Bool, Uuid}`. Optional via `?` suffix
9430                        // reuses `TypeExpr.optional` semantics already in
9431                        // use for flow parameters + body type fields. A
9432                        // duplicate field name in the same block is a
9433                        // parse error (HTTP query strings DO allow
9434                        // multi-value but v1.38.5 binds the first value
9435                        // only — see plan vivo §7 forward-compat).
9436                        //
9437                        // §Fase 37.y (D2 robustness) — declaring `query:`
9438                        // twice on the same axonendpoint silently merged
9439                        // params pre-hardening. Now it's a parse error
9440                        // so an adopter typo / copy-paste mistake
9441                        // surfaces with line + column instead of
9442                        // producing an unexpectedly-augmented endpoint.
9443                        let lbrace_tok = self.consume(TokenType::LBrace)?;
9444                        let block_line = lbrace_tok.line;
9445                        if !node.query_params.is_empty() {
9446                            return Err(ParseError {
9447                                message: format!(
9448                                    "axonendpoint '{}' declares `query: {{ … }}` \
9449                                     more than once. The query-parameter block \
9450                                     is unique per endpoint; combine all params \
9451                                     into a single block (Fase 37.y D2).",
9452                                    node.name,
9453                                ),
9454                                line: lbrace_tok.line,
9455                                column: lbrace_tok.column,
9456                                ..Default::default()
9457                            });
9458                        }
9459                        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9460                            let name_tok = self.consume(TokenType::Identifier)?;
9461                            let field_name = name_tok.value.clone();
9462                            // Duplicate detection within the block.
9463                            if node
9464                                .query_params
9465                                .iter()
9466                                .any(|f| f.name == field_name)
9467                            {
9468                                return Err(ParseError {
9469                                    message: format!(
9470                                        "axonendpoint '{}' declares duplicate \
9471                                         query param '{}' inside `query: {{ … }}`. \
9472                                         Each name must appear at most once \
9473                                         (Fase 37.y D2).",
9474                                        node.name, field_name,
9475                                    ),
9476                                    line: name_tok.line,
9477                                    column: name_tok.column,
9478                                    ..Default::default()
9479                                });
9480                            }
9481                            self.consume(TokenType::Colon)?;
9482                            let type_expr = self.parse_type_expr()?;
9483                            // §Fase 37.y (D2 robustness) — reject generic
9484                            // type expressions on query params. The
9485                            // closed catalog is 5 primitives; container
9486                            // types (`Optional<T>`, `List<T>`, etc.)
9487                            // would mislead the adopter into thinking
9488                            // they bind multi-value query strings
9489                            // (deferred per plan vivo §7) or that
9490                            // `Optional<Text>` is the canonical way to
9491                            // declare an optional query (it's NOT —
9492                            // `Text?` is). Surface the canonical syntax
9493                            // verbatim so the fix is obvious.
9494                            if !type_expr.generic_param.is_empty() {
9495                                let canonical_hint = if type_expr.name == "Optional" {
9496                                    format!(
9497                                        " Use `{}?` (the `?` suffix) for an \
9498                                         optional query param instead of \
9499                                         `Optional<{}>`.",
9500                                        type_expr.generic_param,
9501                                        type_expr.generic_param,
9502                                    )
9503                                } else if type_expr.name == "List" {
9504                                    " Multi-value query params (e.g. `?tag=a&tag=b`) \
9505                                     are honest-deferred from v1.38.5; bind a \
9506                                     single-value `Text` query param and parse \
9507                                     the value inside the flow."
9508                                        .to_string()
9509                                } else {
9510                                    String::new()
9511                                };
9512                                return Err(ParseError {
9513                                    message: format!(
9514                                        "axonendpoint '{}' query param '{}' uses \
9515                                         a generic type `{}<{}>`. Query params \
9516                                         take a primitive type from the closed \
9517                                         catalog ({}); the `?` suffix marks \
9518                                         optional.{} (Fase 37.y D2).",
9519                                        node.name,
9520                                        field_name,
9521                                        type_expr.name,
9522                                        type_expr.generic_param,
9523                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | "),
9524                                        canonical_hint,
9525                                    ),
9526                                    line: type_expr.loc.line,
9527                                    column: type_expr.loc.column,
9528                                    ..Default::default()
9529                                });
9530                            }
9531                            // Validate against the closed catalog. A
9532                            // miss surfaces a Fase 28-style smart-suggest
9533                            // hint when within edit-distance 2.
9534                            if !axonendpoint_is_valid_query_param_type(&type_expr.name) {
9535                                // `smart_suggest::suggest_for` returns
9536                                // pre-formatted prose like
9537                                // "Did you mean `Text`?" or
9538                                // "Did you mean `Text` or `Int`?" (empty
9539                                // when no candidate within edit-distance
9540                                // 2). Concatenate without re-wrapping.
9541                                let hint = crate::smart_suggest::suggest_for(
9542                                    &type_expr.name,
9543                                    AXONENDPOINT_QUERY_PARAM_TYPES,
9544                                );
9545                                let hint_text = if hint.is_empty() {
9546                                    format!(
9547                                        " Expected one of: {}.",
9548                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | ")
9549                                    )
9550                                } else {
9551                                    format!(
9552                                        " {} Expected one of: {}.",
9553                                        hint,
9554                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | ")
9555                                    )
9556                                };
9557                                return Err(ParseError {
9558                                    message: format!(
9559                                        "axonendpoint '{}' query param '{}' has \
9560                                         unsupported type '{}'.{} (Fase 37.y D2).",
9561                                        node.name, field_name, type_expr.name,
9562                                        hint_text,
9563                                    ),
9564                                    line: type_expr.loc.line,
9565                                    column: type_expr.loc.column,
9566                                    ..Default::default()
9567                                });
9568                            }
9569                            node.query_params.push(TypeField {
9570                                name: field_name,
9571                                type_expr,
9572                                loc: Loc {
9573                                    line: name_tok.line,
9574                                    column: name_tok.column,
9575                                },
9576                            });
9577                            // Trailing comma is optional; the next loop
9578                            // iteration handles `}` cleanly. Accept both
9579                            // `name: Type, name: Type` AND `name: Type
9580                            // name: Type` (the existing parser style is
9581                            // forgiving about list separators).
9582                            if self.check(TokenType::Comma) {
9583                                self.advance();
9584                            }
9585                            let _ = block_line; // suppress unused warning
9586                        }
9587                        self.consume(TokenType::RBrace)?;
9588                    },
9589                    "execute" => node.execute_flow = self.consume_any_ident_or_kw()?.value.clone(),
9590                    "output" => {
9591                        // §Fase 38.x.f — promote axonendpoint `output:`
9592                        // parsing from a single token to the full
9593                        // generic-aware type expression (mirroring
9594                        // `parse_step` for FlowStep::Step which already
9595                        // uses `parse_output_type_string`).
9596                        //
9597                        // Pre-38.x.f: `output: List<Item>` captured only
9598                        // `"List"`, dropping `<Item>` (next tokens were
9599                        // either left unconsumed or absorbed by the
9600                        // following field). v1.39.0's narrow cardinality
9601                        // gate happened to fire correctly for `output: T`
9602                        // + retrieve-tail because the singular-detection
9603                        // path used `!starts_with("List<")` — but the
9604                        // SYMMETRIC `output: List<T>` + singular-tail
9605                        // case (38.x.f D3) needs the FULL `List<T>`
9606                        // shape captured; without it the gate sees
9607                        // `"List"` and misclassifies as Singular.
9608                        node.output_type = self.parse_output_type_string()?;
9609                    }
9610                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
9611                    // §Fase 83.a — the `cors: <Name>` reference.
9612                    "cors" => node.cors_ref = self.consume_any_ident_or_kw()?.value.clone(),
9613                    "retries" => node.retries = self.parse_optional_int(),
9614                    "timeout" => {
9615                        let t = self.current().clone();
9616                        self.advance();
9617                        node.timeout = t.value.clone();
9618                    }
9619                    "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
9620                    "replay" => {
9621                        // §Fase 32.h (D9 plan-vivo) — Replay-token binding.
9622                        // Boolean `replay: true | false`. Default (when
9623                        // omitted) is method-derived at deploy-time:
9624                        // POST/PUT → true, GET/DELETE → false. Explicit
9625                        // declaration sets `replay_explicit = true` so
9626                        // the runtime knows NOT to override.
9627                        let value_tok = self.consume(TokenType::Bool)?;
9628                        node.replay = value_tok.value.eq_ignore_ascii_case("true");
9629                        node.replay_explicit = true;
9630                    }
9631                    // §Fase 89.a — `public: true | false`, the explicit
9632                    // authorization-coverage opt-out (doctrine
9633                    // `every_boundary_is_guarded`). Mirrors `replay:`'s bool
9634                    // parse. Default false; the §89.b rule (`axon-T890`)
9635                    // requires a covering discipline OR `public: true`.
9636                    "public" => {
9637                        let value_tok = self.consume(TokenType::Bool)?;
9638                        node.public = value_tok.value.eq_ignore_ascii_case("true");
9639                    }
9640                    "requires" => {
9641                        // §Fase 32.g (D8) — Auth scope per axonendpoint.
9642                        // Closed slug grammar
9643                        // `^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$` enforced
9644                        // at parse time with smart-suggest-style hint.
9645                        // Empty list means "no auth gate" (D9 backwards-
9646                        // compat). Cross-stack with Python parser.
9647                        let bracket_tok = self.current().clone();
9648                        let items = self.parse_bracketed_dot_identifiers()?;
9649                        for slug in &items {
9650                            if !is_valid_capability_slug(slug) {
9651                                return Err(ParseError {
9652                                    message: format!(
9653                                        "Invalid capability slug '{slug}' in axonendpoint '{}' \
9654                                         `requires:`. Capability slugs must match \
9655                                         ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
9656                                         lowercase identifiers starting with a letter. Examples: \
9657                                         `admin`, `legal.read`, `hipaa.phi.read`.",
9658                                        node.name
9659                                    ),
9660                                    line: bracket_tok.line,
9661                                    column: bracket_tok.column,
9662                                    ..Default::default()
9663                                });
9664                            }
9665                        }
9666                        node.requires_capabilities = items;
9667                    }
9668                    // §Fase 30.b — HTTP transport enum (D2 closed) + keepalive (D6 closed).
9669                    // Mirrors `axon/compiler/parser.py` `_parse_axonendpoint`.
9670                    // Drift-gate corpus verifies byte-identical parse cross-stack.
9671                    "transport" => {
9672                        let value_tok = self.consume_any_ident_or_kw()?;
9673                        let value = &value_tok.value;
9674                        if !axonendpoint_is_valid_transport(value) {
9675                            let hint = crate::smart_suggest::suggest_for(
9676                                value,
9677                                AXONENDPOINT_TRANSPORT_VALUES,
9678                            );
9679                            let base = format!(
9680                                "Invalid transport '{}' in axonendpoint '{}'.",
9681                                value, node.name
9682                            );
9683                            let message = if hint.is_empty() {
9684                                format!("{base} expected json | sse | ndjson, found {value}")
9685                            } else {
9686                                format!(
9687                                    "{base} {hint} (expected json | sse | ndjson, found {value})"
9688                                )
9689                            };
9690                            return Err(ParseError {
9691                                message,
9692                                line: value_tok.line,
9693                                column: value_tok.column,
9694                                ..Default::default()
9695                            });
9696                        }
9697                        node.transport = value.clone();
9698                        // §Fase 31.b D1 — mark the field as explicitly
9699                        // declared so the type-checker's implicit-transport
9700                        // inference knows NOT to override this value with
9701                        // the produces_stream-driven inference.
9702                        node.transport_explicit = true;
9703                        // §Fase 33.z.k.b (v1.28.0) — Optional dialect
9704                        // parametrization: `transport: sse(<dialect>)`.
9705                        // Only valid when the base value is `sse`
9706                        // (json + ndjson dialects are the dialects
9707                        // themselves; `json(<x>)` / `ndjson(<x>)`
9708                        // would be parse errors caught below).
9709                        if self.check(TokenType::LParen) {
9710                            if value != "sse" {
9711                                let tok = self.current().clone();
9712                                return Err(ParseError {
9713                                    message: format!(
9714                                        "Dialect parametrization \
9715                                         `transport: {value}(<dialect>)` is \
9716                                         only valid for `sse`; got \
9717                                         `{value}` in axonendpoint '{}'.",
9718                                        node.name
9719                                    ),
9720                                    line: tok.line,
9721                                    column: tok.column,
9722                                    ..Default::default()
9723                                });
9724                            }
9725                            self.advance(); // consume LParen
9726                            let dialect_tok = self.consume_any_ident_or_kw()?;
9727                            let dialect = dialect_tok.value.clone();
9728                            if !AXONENDPOINT_TRANSPORT_DIALECTS
9729                                .iter()
9730                                .any(|&d| d == dialect)
9731                            {
9732                                let hint = crate::smart_suggest::suggest_for(
9733                                    &dialect,
9734                                    AXONENDPOINT_TRANSPORT_DIALECTS,
9735                                );
9736                                let base = format!(
9737                                    "Invalid SSE dialect '{dialect}' in axonendpoint '{}'.",
9738                                    node.name
9739                                );
9740                                let message = if hint.is_empty() {
9741                                    format!(
9742                                        "{base} expected axon | openai | kimi | glm | anthropic, found {dialect}"
9743                                    )
9744                                } else {
9745                                    format!(
9746                                        "{base} {hint} (expected axon | openai | kimi | glm | anthropic, found {dialect})"
9747                                    )
9748                                };
9749                                return Err(ParseError {
9750                                    message,
9751                                    line: dialect_tok.line,
9752                                    column: dialect_tok.column,
9753                                    ..Default::default()
9754                                });
9755                            }
9756                            // Closing RParen.
9757                            let rparen_tok = self.current().clone();
9758                            if !self.check(TokenType::RParen) {
9759                                return Err(ParseError {
9760                                    message: format!(
9761                                        "Expected `)` after dialect name \
9762                                         in axonendpoint '{}' \
9763                                         (transport: sse(<dialect>) grammar).",
9764                                        node.name
9765                                    ),
9766                                    line: rparen_tok.line,
9767                                    column: rparen_tok.column,
9768                                    ..Default::default()
9769                                });
9770                            }
9771                            self.advance(); // consume RParen
9772                            node.transport_dialect = dialect;
9773                        }
9774                    }
9775                    "keepalive" => {
9776                        // Accepts either a DURATION token (e.g. `15s`) or
9777                        // an ident-like token. Validation against the
9778                        // closed enum {5s, 15s, 30s, 60s} happens after.
9779                        let value_tok = self.current().clone();
9780                        self.advance();
9781                        let value = &value_tok.value;
9782                        if !axonendpoint_is_valid_keepalive(value) {
9783                            let hint = crate::smart_suggest::suggest_for(
9784                                value,
9785                                AXONENDPOINT_KEEPALIVE_VALUES,
9786                            );
9787                            let base = format!(
9788                                "Invalid keepalive '{}' in axonendpoint '{}'.",
9789                                value, node.name
9790                            );
9791                            let message = if hint.is_empty() {
9792                                format!("{base} expected 5s | 15s | 30s | 60s, found {value}")
9793                            } else {
9794                                format!(
9795                                    "{base} {hint} (expected 5s | 15s | 30s | 60s, found {value})"
9796                                )
9797                            };
9798                            return Err(ParseError {
9799                                message,
9800                                line: value_tok.line,
9801                                column: value_tok.column,
9802                                ..Default::default()
9803                            });
9804                        }
9805                        node.keepalive = value.clone();
9806                    }
9807                    "backend" => {
9808                        // §Fase 36.d (D2) — declared execution backend.
9809                        // Closed catalog `CANONICAL_PROVIDERS ∪ {auto,
9810                        // stub}`; an unknown name is a parse error with
9811                        // a smart-suggest hint (the same discipline as
9812                        // `method`/`transport`/`keepalive`). The
9813                        // type-checker re-validates defensively for
9814                        // ASTs built outside the parser (LSP, tests).
9815                        let value_tok = self.consume_any_ident_or_kw()?;
9816                        let value = &value_tok.value;
9817                        if !axonendpoint_is_valid_backend(value) {
9818                            let hint = crate::smart_suggest::suggest_for(
9819                                value,
9820                                AXONENDPOINT_BACKEND_VALUES,
9821                            );
9822                            let expected = AXONENDPOINT_BACKEND_VALUES.join(" | ");
9823                            let base = format!(
9824                                "Invalid backend '{}' in axonendpoint '{}'.",
9825                                value, node.name
9826                            );
9827                            let message = if hint.is_empty() {
9828                                format!("{base} expected {expected}, found {value}")
9829                            } else {
9830                                format!(
9831                                    "{base} {hint} (expected {expected}, found {value})"
9832                                )
9833                            };
9834                            return Err(ParseError {
9835                                message,
9836                                line: value_tok.line,
9837                                column: value_tok.column,
9838                                ..Default::default()
9839                            });
9840                        }
9841                        node.backend = value.clone();
9842                    }
9843                    _ => self.skip_value(),
9844                }
9845            } else if self.check(TokenType::LBrace) {
9846                self.skip_braced_block()?;
9847            }
9848        }
9849        self.consume(TokenType::RBrace)?;
9850        Ok(node)
9851    }
9852
9853    // ── Numeric helpers for Tier 2 field parsing ────────────────────
9854
9855    fn parse_optional_int(&mut self) -> Option<i64> {
9856        let tok = self.current().clone();
9857        match tok.ttype {
9858            TokenType::Integer => {
9859                self.advance();
9860                tok.value.parse::<i64>().ok()
9861            }
9862            _ => {
9863                self.advance();
9864                None
9865            }
9866        }
9867    }
9868
9869    fn parse_optional_float(&mut self) -> Option<f64> {
9870        let tok = self.current().clone();
9871        match tok.ttype {
9872            TokenType::Float | TokenType::Integer => {
9873                self.advance();
9874                tok.value.parse::<f64>().ok()
9875            }
9876            _ => {
9877                self.advance();
9878                None
9879            }
9880        }
9881    }
9882
9883    // ── LAMBDA DATA (ΛD) ──────────────────────────────────────────
9884
9885    fn parse_lambda_data(&mut self) -> Result<LambdaDataDefinition, ParseError> {
9886        let tok = self.consume(TokenType::Lambda)?;
9887        let name = self.consume(TokenType::Identifier)?;
9888        self.consume(TokenType::LBrace)?;
9889
9890        let mut node = LambdaDataDefinition {
9891            name: name.value.clone(),
9892            ontology: String::new(),
9893            certainty: 1.0,
9894            temporal_frame_start: String::new(),
9895            temporal_frame_end: String::new(),
9896            provenance: String::new(),
9897            derivation: String::new(),
9898            loc: Loc {
9899                line: tok.line,
9900                column: tok.column,
9901            },
9902            leading_trivia: Vec::new(),
9903            trailing_trivia: Vec::new(),
9904        };
9905
9906        while !self.check(TokenType::RBrace) {
9907            let field = self.current().clone();
9908            match field.ttype {
9909                TokenType::Ontology => {
9910                    self.advance();
9911                    self.consume(TokenType::Colon)?;
9912                    node.ontology = self.consume(TokenType::StringLit)?.value.clone();
9913                }
9914                TokenType::Certainty => {
9915                    self.advance();
9916                    self.consume(TokenType::Colon)?;
9917                    let val = self.current().clone();
9918                    match val.ttype {
9919                        TokenType::Float => {
9920                            self.advance();
9921                            node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
9922                        }
9923                        TokenType::Integer => {
9924                            self.advance();
9925                            node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
9926                        }
9927                        _ => {
9928                            return Err(ParseError {
9929                                message: format!(
9930                                    "Expected number for certainty, got '{}'",
9931                                    val.value
9932                                ),
9933                                line: val.line,
9934                                column: val.column,
9935                                                            ..Default::default()
9936                            });
9937                        }
9938                    }
9939                }
9940                TokenType::TemporalFrame => {
9941                    self.advance();
9942                    self.consume(TokenType::Colon)?;
9943                    node.temporal_frame_start = self.consume(TokenType::StringLit)?.value.clone();
9944                    // Optional second string for end frame
9945                    if self.check(TokenType::StringLit) {
9946                        node.temporal_frame_end = self.consume(TokenType::StringLit)?.value.clone();
9947                    }
9948                }
9949                TokenType::Provenance => {
9950                    self.advance();
9951                    self.consume(TokenType::Colon)?;
9952                    node.provenance = self.consume(TokenType::StringLit)?.value.clone();
9953                }
9954                TokenType::Derivation => {
9955                    self.advance();
9956                    self.consume(TokenType::Colon)?;
9957                    let d = self.current().clone();
9958                    self.advance();
9959                    node.derivation = d.value.clone();
9960                }
9961                _ => {
9962                    // Skip unknown fields gracefully
9963                    self.advance();
9964                    if self.check(TokenType::Colon) {
9965                        self.advance();
9966                        self.skip_value();
9967                    }
9968                }
9969            }
9970        }
9971
9972        self.consume(TokenType::RBrace)?;
9973        Ok(node)
9974    }
9975
9976    fn parse_lambda_data_apply(&mut self) -> Result<LambdaDataApplyNode, ParseError> {
9977        let tok = self.consume(TokenType::Lambda)?;
9978        let lambda_name = self.consume(TokenType::Identifier)?;
9979
9980        // Expect "on" keyword (parsed as identifier since it's not reserved)
9981        let on_tok = self.current().clone();
9982        self.advance();
9983        if on_tok.value != "on" {
9984            return Err(ParseError {
9985                message: format!(
9986                    "Expected 'on' after lambda data name in flow step, got '{}'",
9987                    on_tok.value
9988                ),
9989                line: on_tok.line,
9990                column: on_tok.column,
9991                            ..Default::default()
9992            });
9993        }
9994
9995        let target = self.current().clone();
9996        self.advance();
9997
9998        let mut output_type = String::new();
9999        if self.check(TokenType::Arrow) {
10000            self.advance();
10001            output_type = self.consume(TokenType::Identifier)?.value.clone();
10002        }
10003
10004        Ok(LambdaDataApplyNode {
10005            lambda_data_name: lambda_name.value.clone(),
10006            target: target.value.clone(),
10007            output_type,
10008            loc: Loc {
10009                line: tok.line,
10010                column: tok.column,
10011            },
10012        })
10013    }
10014
10015    // ── GENERIC (Tier 2+) ────────────────────────────────────────
10016
10017    fn parse_generic_declaration(&mut self) -> Result<Declaration, ParseError> {
10018        let kw_tok = self.current().clone();
10019        self.advance(); // consume keyword
10020
10021        // Try to consume a name (identifier or keyword-as-name)
10022        let name = if self.current().ttype == TokenType::Identifier {
10023            let n = self.current().value.clone();
10024            self.advance();
10025            n
10026        } else if !self.check(TokenType::LBrace)
10027            && !self.check(TokenType::LParen)
10028            && !self.check(TokenType::Eof)
10029            && self
10030                .current()
10031                .value
10032                .chars()
10033                .all(|c| c.is_alphanumeric() || c == '_')
10034        {
10035            let n = self.current().value.clone();
10036            self.advance();
10037            n
10038        } else {
10039            String::new()
10040        };
10041
10042        // Skip optional parens: (...)
10043        if self.check(TokenType::LParen) {
10044            self.advance();
10045            let mut depth = 1u32;
10046            while depth > 0 && !self.check(TokenType::Eof) {
10047                if self.check(TokenType::LParen) {
10048                    depth += 1;
10049                } else if self.check(TokenType::RParen) {
10050                    depth -= 1;
10051                }
10052                self.advance();
10053            }
10054        }
10055
10056        // Skip tokens until LBrace or next declaration
10057        while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
10058            if self.check(TokenType::Eof) {
10059                break;
10060            }
10061            self.advance();
10062        }
10063
10064        // Skip braced block if present
10065        if self.check(TokenType::LBrace) {
10066            self.skip_braced_block()?;
10067        }
10068
10069        Ok(Declaration::Generic(GenericDeclaration {
10070            keyword: kw_tok.value,
10071            name,
10072            loc: Loc {
10073                line: kw_tok.line,
10074                column: kw_tok.column,
10075            },
10076            leading_trivia: Vec::new(),
10077            trailing_trivia: Vec::new(),
10078        }))
10079    }
10080
10081    // ──────────────────────────────────────────────────────────────────
10082    //  §λ-L-E Fase 13 — Mobile Typed Channels parsers
10083    //  (paper_mobile_channels.md §3 + plan/fase_13)
10084    //  Direct port of axon/compiler/parser.py:_parse_channel/emit/publish/discover.
10085    // ──────────────────────────────────────────────────────────────────
10086
10087    /// Parse: `channel Name { message, qos, lifetime, persistence, shield }`.
10088    fn parse_channel(&mut self) -> Result<ChannelDefinition, ParseError> {
10089        let tok = self.consume(TokenType::Channel)?;
10090        let name = self.consume(TokenType::Identifier)?.value;
10091        let mut node = ChannelDefinition {
10092            name: name.clone(),
10093            message: String::new(),
10094            qos: "at_least_once".to_string(),
10095            lifetime: "affine".to_string(),
10096            persistence: "ephemeral".to_string(),
10097            shield_ref: String::new(),
10098            loc: Loc {
10099                line: tok.line,
10100                column: tok.column,
10101            },
10102            leading_trivia: Vec::new(),
10103            trailing_trivia: Vec::new(),
10104        };
10105        self.consume(TokenType::LBrace)?;
10106        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10107            let field_tok = self.current().clone();
10108            let field_name = field_tok.value.clone();
10109            self.advance();
10110            if !self.check(TokenType::Colon) {
10111                if self.check(TokenType::LBrace) {
10112                    self.skip_braced_block()?;
10113                }
10114                continue;
10115            }
10116            self.advance();
10117            match field_name.as_str() {
10118                "message" => node.message = self.parse_channel_message_type()?,
10119                "qos" => {
10120                    let q_tok = self.consume_any_ident_or_kw()?;
10121                    if !matches!(
10122                        q_tok.value.as_str(),
10123                        "at_most_once" | "at_least_once" | "exactly_once" | "broadcast" | "queue"
10124                    ) {
10125                        return Err(ParseError {
10126                            message: format!(
10127                                "Invalid qos '{}' in channel '{}' — \
10128                                 expected at_most_once | at_least_once | \
10129                                 exactly_once | broadcast | queue",
10130                                q_tok.value, name
10131                            ),
10132                            line: q_tok.line,
10133                            column: q_tok.column,
10134                                                    ..Default::default()
10135                        });
10136                    }
10137                    node.qos = q_tok.value;
10138                }
10139                "lifetime" => {
10140                    let lt_tok = self.consume_any_ident_or_kw()?;
10141                    if !matches!(lt_tok.value.as_str(), "linear" | "affine" | "persistent") {
10142                        return Err(ParseError {
10143                            message: format!(
10144                                "Invalid lifetime '{}' in channel '{}' — \
10145                                 expected linear | affine | persistent",
10146                                lt_tok.value, name
10147                            ),
10148                            line: lt_tok.line,
10149                            column: lt_tok.column,
10150                                                    ..Default::default()
10151                        });
10152                    }
10153                    node.lifetime = lt_tok.value;
10154                }
10155                "persistence" => {
10156                    let p_tok = self.consume_any_ident_or_kw()?;
10157                    if !matches!(p_tok.value.as_str(), "ephemeral" | "persistent_axonstore") {
10158                        return Err(ParseError {
10159                            message: format!(
10160                                "Invalid persistence '{}' in channel '{}' — \
10161                                 expected ephemeral | persistent_axonstore",
10162                                p_tok.value, name
10163                            ),
10164                            line: p_tok.line,
10165                            column: p_tok.column,
10166                                                    ..Default::default()
10167                        });
10168                    }
10169                    node.persistence = p_tok.value;
10170                }
10171                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
10172                _ => self.skip_value(),
10173            }
10174        }
10175        self.consume(TokenType::RBrace)?;
10176        Ok(node)
10177    }
10178
10179    /// Parse a `message:` value, supporting nested `Channel<…>`
10180    /// (second-order session types — paper §3.3).
10181    fn parse_channel_message_type(&mut self) -> Result<String, ParseError> {
10182        let head = self.consume(TokenType::Identifier)?;
10183        let mut spelling = head.value;
10184        if self.check(TokenType::Lt) {
10185            self.advance();
10186            let inner = self.parse_channel_message_type()?;
10187            self.consume(TokenType::Gt)?;
10188            spelling = format!("{}<{}>", spelling, inner);
10189        }
10190        Ok(spelling)
10191    }
10192
10193    /// Parse: `emit ChannelName(value_ref)` — Chan-Output / Chan-Mobility.
10194    ///
10195    /// `value_ref` accepts a bare identifier (variable / channel name for
10196    /// mobility) or a dotted path (`Step.output.field`) referencing a prior
10197    /// step result (Fase 13.i — runtime resolves via ContextManager).
10198    fn parse_emit_step(&mut self) -> Result<FlowStep, ParseError> {
10199        let tok = self.consume(TokenType::Emit)?;
10200        let channel = self.consume(TokenType::Identifier)?.value;
10201        self.consume(TokenType::LParen)?;
10202        let value = self.parse_emit_value_ref()?;
10203        self.consume(TokenType::RParen)?;
10204        Ok(FlowStep::Emit(EmitStatement {
10205            channel_ref: channel,
10206            value_ref: value,
10207            loc: Loc {
10208                line: tok.line,
10209                column: tok.column,
10210            },
10211        }))
10212    }
10213
10214    /// §Fase 92.b — parse `mint <Credential> as <binding>`. The credential
10215    /// reference must resolve to a declared `credential` (`axon-T895`,
10216    /// type-checker); the binding is a fresh flow-scoped name receiving the
10217    /// raw bearer string. Both tokens are required — a `mint` with no
10218    /// binding would mint authority into the void.
10219    fn parse_mint_step(&mut self) -> Result<FlowStep, ParseError> {
10220        let tok = self.consume(TokenType::Mint)?;
10221        let credential_ref = self.consume(TokenType::Identifier)?.value;
10222        self.consume(TokenType::As)?;
10223        let binding = self.consume(TokenType::Identifier)?.value;
10224        Ok(FlowStep::Mint(MintStep {
10225            credential_ref,
10226            binding,
10227            loc: Loc {
10228                line: tok.line,
10229                column: tok.column,
10230            },
10231        }))
10232    }
10233
10234    /// §Fase 94.b — parse `rotate <SecretsStore> [where "<filter>"] with
10235    /// <Tool> as <binding>` (doctrine `rotation_without_revelation`).
10236    ///
10237    /// All three anchors are grammar, not convention: the store names WHAT
10238    /// may rotate (a `backend: secrets` class view — `axon-T898` in the
10239    /// type-checker), the tool names WHO performs the exchange
10240    /// (`axon-T899`), and the binding receives the metadata-only summary —
10241    /// a `rotate` without a binding would renew authority with no
10242    /// observable outcome, so `as` is REQUIRED (the `mint` posture). The
10243    /// `where` filter is optional (§67 string grammar, proven against the
10244    /// synthesized metadata schema); omitting it rotates the WHOLE class —
10245    /// the deliberate post-breach bulk shape. `with` is a soft keyword
10246    /// (not a lexer token): reserving it globally would break every
10247    /// adopter identifier named `with`.
10248    fn parse_rotate_step(&mut self) -> Result<FlowStep, ParseError> {
10249        let tok = self.consume(TokenType::Rotate)?;
10250        let store_ref = self.consume(TokenType::Identifier)?.value;
10251        let mut where_expr = String::new();
10252        if self.check(TokenType::Where) {
10253            self.advance();
10254            where_expr = self.consume(TokenType::StringLit)?.value.clone();
10255        }
10256        let with_tok = self.current().clone();
10257        if with_tok.value != "with" {
10258            return Err(ParseError {
10259                message: format!(
10260                    "Expected `with <Tool>` after `rotate {store_ref}{}`, found '{}'. \
10261                     A rotation names the tool that performs the renewal exchange: \
10262                     `rotate {store_ref} [where \"<filter>\"] with <Tool> as <binding>`.",
10263                    if where_expr.is_empty() { "" } else { " where …" },
10264                    with_tok.value
10265                ),
10266                line: with_tok.line,
10267                column: with_tok.column,
10268                ..Default::default()
10269            });
10270        }
10271        self.advance();
10272        let tool_ref = self.consume(TokenType::Identifier)?.value;
10273        self.consume(TokenType::As)?;
10274        let binding = self.consume(TokenType::Identifier)?.value;
10275        Ok(FlowStep::Rotate(RotateStep {
10276            store_ref,
10277            where_expr,
10278            tool_ref,
10279            binding,
10280            loc: Loc {
10281                line: tok.line,
10282                column: tok.column,
10283            },
10284        }))
10285    }
10286
10287    /// Parse: `IDENTIFIER ('.' (IDENTIFIER | keyword))*` → dot-joined string
10288    /// (Fase 13.i).
10289    ///
10290    /// Mirrors the Python `_parse_emit_value_ref` helper exactly so the IR
10291    /// JSON for `emit Hello(Build.output)` is byte-identical between the
10292    /// two reference implementations.
10293    ///
10294    /// The HEAD must be a real ``Identifier``. Subsequent segments after a
10295    /// `.` may be identifiers OR keywords — common field names like
10296    /// ``output``, ``result``, ``message``, ``state``, etc. are reserved
10297    /// words in Axon but adopters must be able to write them as
10298    /// dotted-access segments. The accepting predicate:
10299    ///   - the lexer carried a non-empty `value` (every Word-like token does)
10300    ///   - the value's first byte is a letter or underscore (filters out
10301    ///     punctuation tokens such as ',', '{', etc.)
10302    fn parse_emit_value_ref(&mut self) -> Result<String, ParseError> {
10303        let head = self.consume(TokenType::Identifier)?.value;
10304        let mut parts = vec![head];
10305        while self.check(TokenType::Dot) {
10306            self.advance(); // consume '.'
10307            let next_tok = self.current().clone();
10308            let valid = !next_tok.value.is_empty()
10309                && next_tok.value.as_bytes()[0].is_ascii_alphabetic()
10310                || next_tok.value.starts_with('_');
10311            if !valid {
10312                return Err(ParseError {
10313                    message: format!(
10314                        "Expected identifier or keyword after '.' in dotted \
10315                         access, found {:?}",
10316                        next_tok.value
10317                    ),
10318                    line: next_tok.line,
10319                    column: next_tok.column,
10320                                    ..Default::default()
10321                });
10322            }
10323            self.advance();
10324            parts.push(next_tok.value);
10325        }
10326        Ok(parts.join("."))
10327    }
10328
10329    /// Parse: `publish ChannelName within ShieldName` — Publish-Ext (D8).
10330    fn parse_publish_step(&mut self) -> Result<FlowStep, ParseError> {
10331        let tok = self.consume(TokenType::Publish)?;
10332        let channel = self.consume(TokenType::Identifier)?.value;
10333        self.consume(TokenType::Within)?;
10334        let shield = self.consume(TokenType::Identifier)?.value;
10335        Ok(FlowStep::Publish(PublishStatement {
10336            channel_ref: channel,
10337            shield_ref: shield,
10338            loc: Loc {
10339                line: tok.line,
10340                column: tok.column,
10341            },
10342        }))
10343    }
10344
10345    /// Parse: `discover ChannelName as alias` — dual of publish.
10346    fn parse_discover_step(&mut self) -> Result<FlowStep, ParseError> {
10347        let tok = self.consume(TokenType::Discover)?;
10348        let cap = self.consume(TokenType::Identifier)?.value;
10349        self.consume(TokenType::As)?;
10350        let alias = self.consume(TokenType::Identifier)?.value;
10351        Ok(FlowStep::Discover(DiscoverStatement {
10352            capability_ref: cap,
10353            alias,
10354            loc: Loc {
10355                line: tok.line,
10356                column: tok.column,
10357            },
10358        }))
10359    }
10360}
10361
10362// ── §λ-L-E Fase 13 — Mobile Typed Channels parser tests ─────────────────────
10363
10364#[cfg(test)]
10365mod fase13_parser_tests {
10366    use super::*;
10367    use crate::lexer::Lexer;
10368
10369    fn parse(src: &str) -> Result<Program, ParseError> {
10370        let tokens = Lexer::new(src, "<test>").tokenize().expect("lex");
10371        Parser::new(tokens).parse()
10372    }
10373
10374    #[test]
10375    fn channel_full_parses() {
10376        let src = r#"channel C { message: Order qos: at_least_once lifetime: affine persistence: ephemeral shield: Gate }"#;
10377        let prog = parse(src).expect("parse");
10378        match &prog.declarations[0] {
10379            Declaration::Channel(c) => {
10380                assert_eq!(c.name, "C");
10381                assert_eq!(c.message, "Order");
10382                assert_eq!(c.qos, "at_least_once");
10383                assert_eq!(c.lifetime, "affine");
10384                assert_eq!(c.persistence, "ephemeral");
10385                assert_eq!(c.shield_ref, "Gate");
10386            }
10387            _ => panic!("expected ChannelDefinition"),
10388        }
10389    }
10390
10391    #[test]
10392    fn channel_defaults_match_paper_d1() {
10393        let prog = parse("channel C { message: Order }").expect("parse");
10394        if let Declaration::Channel(c) = &prog.declarations[0] {
10395            assert_eq!(c.qos, "at_least_once"); // default
10396            assert_eq!(c.lifetime, "affine"); // D1 default
10397            assert_eq!(c.persistence, "ephemeral");
10398            assert_eq!(c.shield_ref, "");
10399        } else {
10400            panic!("expected ChannelDefinition");
10401        }
10402    }
10403
10404    #[test]
10405    fn channel_second_order_message_type_parses() {
10406        let prog = parse("channel C { message: Channel<Order> }").expect("parse");
10407        if let Declaration::Channel(c) = &prog.declarations[0] {
10408            assert_eq!(c.message, "Channel<Order>");
10409        } else {
10410            panic!("expected ChannelDefinition");
10411        }
10412    }
10413
10414    #[test]
10415    fn channel_nested_channel_message_type_parses() {
10416        let prog = parse("channel C { message: Channel<Channel<Order>> }").expect("parse");
10417        if let Declaration::Channel(c) = &prog.declarations[0] {
10418            assert_eq!(c.message, "Channel<Channel<Order>>");
10419        } else {
10420            panic!("expected ChannelDefinition");
10421        }
10422    }
10423
10424    #[test]
10425    fn channel_invalid_qos_rejected() {
10426        let err = parse("channel C { message: T qos: bogus }").unwrap_err();
10427        assert!(err.message.contains("Invalid qos"), "got {}", err.message);
10428    }
10429
10430    #[test]
10431    fn channel_invalid_lifetime_rejected() {
10432        let err = parse("channel C { message: T lifetime: eternal }").unwrap_err();
10433        assert!(
10434            err.message.contains("Invalid lifetime"),
10435            "got {}",
10436            err.message
10437        );
10438    }
10439
10440    #[test]
10441    fn channel_invalid_persistence_rejected() {
10442        let err = parse("channel C { message: T persistence: forever }").unwrap_err();
10443        assert!(
10444            err.message.contains("Invalid persistence"),
10445            "got {}",
10446            err.message
10447        );
10448    }
10449
10450    #[test]
10451    fn emit_value_parses() {
10452        let src = "flow f() -> Out { emit C(payload) }";
10453        let prog = parse(src).expect("parse");
10454        if let Declaration::Flow(f) = &prog.declarations[0] {
10455            match &f.body[0] {
10456                FlowStep::Emit(e) => {
10457                    assert_eq!(e.channel_ref, "C");
10458                    assert_eq!(e.value_ref, "payload");
10459                }
10460                other => panic!("expected Emit, got {:?}", other),
10461            }
10462        } else {
10463            panic!("expected Flow");
10464        }
10465    }
10466
10467    #[test]
10468    fn publish_within_shield_parses() {
10469        let src = "flow f() -> Cap { publish C within Gate }";
10470        let prog = parse(src).expect("parse");
10471        if let Declaration::Flow(f) = &prog.declarations[0] {
10472            match &f.body[0] {
10473                FlowStep::Publish(p) => {
10474                    assert_eq!(p.channel_ref, "C");
10475                    assert_eq!(p.shield_ref, "Gate");
10476                }
10477                other => panic!("expected Publish, got {:?}", other),
10478            }
10479        } else {
10480            panic!("expected Flow");
10481        }
10482    }
10483
10484    #[test]
10485    fn discover_with_alias_parses() {
10486        let src = "flow f() -> Out { discover C as ch }";
10487        let prog = parse(src).expect("parse");
10488        if let Declaration::Flow(f) = &prog.declarations[0] {
10489            match &f.body[0] {
10490                FlowStep::Discover(d) => {
10491                    assert_eq!(d.capability_ref, "C");
10492                    assert_eq!(d.alias, "ch");
10493                }
10494                other => panic!("expected Discover, got {:?}", other),
10495            }
10496        } else {
10497            panic!("expected Flow");
10498        }
10499    }
10500
10501    #[test]
10502    fn listen_typed_ref_sets_flag_true() {
10503        let src = "daemon D() { goal: \"x\" listen C as ev { } }";
10504        let prog = parse(src).expect("parse");
10505        if let Declaration::Daemon(d) = &prog.declarations[0] {
10506            assert_eq!(d.listeners.len(), 1);
10507            assert_eq!(d.listeners[0].channel, "C");
10508            assert!(d.listeners[0].channel_is_ref, "typed ref ⇒ true");
10509        } else {
10510            panic!("expected Daemon");
10511        }
10512    }
10513
10514    #[test]
10515    fn listen_string_topic_legacy_flag_false() {
10516        let src = "daemon D() { goal: \"x\" listen \"orders\" as ev { } }";
10517        let prog = parse(src).expect("parse");
10518        if let Declaration::Daemon(d) = &prog.declarations[0] {
10519            assert_eq!(d.listeners.len(), 1);
10520            assert_eq!(d.listeners[0].channel, "orders");
10521            assert!(!d.listeners[0].channel_is_ref, "string topic ⇒ false");
10522        } else {
10523            panic!("expected Daemon");
10524        }
10525    }
10526
10527    // ── Fase 13.i — emit value_ref accepts dotted access ───────────
10528
10529    fn extract_first_emit(prog: &Program) -> &EmitStatement {
10530        if let Declaration::Flow(f) = &prog.declarations[0] {
10531            if let FlowStep::Emit(e) = &f.body[0] {
10532                return e;
10533            }
10534        }
10535        panic!("expected emit statement at flow body[0]");
10536    }
10537
10538    #[test]
10539    fn emit_accepts_bare_identifier_value_ref() {
10540        // Pre-13.i baseline — must keep working.
10541        let prog = parse("flow f() -> Out { emit Hello(payload) }").expect("parse");
10542        let emit = extract_first_emit(&prog);
10543        assert_eq!(emit.channel_ref, "Hello");
10544        assert_eq!(emit.value_ref, "payload");
10545    }
10546
10547    #[test]
10548    fn emit_accepts_two_segment_dotted_value_ref() {
10549        // The exact case adopters reported as broken before 13.i.
10550        let prog = parse("flow f() -> Out { emit Hello(Build.output) }").expect("parse");
10551        let emit = extract_first_emit(&prog);
10552        assert_eq!(emit.value_ref, "Build.output");
10553    }
10554
10555    #[test]
10556    fn emit_accepts_three_segment_nested_dotted_value_ref() {
10557        let prog = parse("flow f() -> Out { emit Score(Analyze.result.score) }").expect("parse");
10558        let emit = extract_first_emit(&prog);
10559        assert_eq!(emit.value_ref, "Analyze.result.score");
10560    }
10561
10562    #[test]
10563    fn emit_dotted_with_trailing_dot_fails() {
10564        // Trailing `.` must still error — every '.' demands an identifier.
10565        let result = parse("flow f() -> Out { emit Hello(Build.) }");
10566        assert!(result.is_err(), "expected parse error for trailing dot");
10567    }
10568}
10569
10570// ── §Fase 14.a — declaration_trivia parallel channel tests ──────────────────
10571
10572#[cfg(test)]
10573mod fase14a_declaration_trivia_tests {
10574    use super::*;
10575    use crate::lexer::Lexer;
10576    use crate::tokens::TriviaKind;
10577
10578    fn parse(src: &str) -> Program {
10579        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
10580        Parser::new(toks).parse().expect("parse")
10581    }
10582
10583    #[test]
10584    fn no_comments_means_empty_trivia_per_decl() {
10585        let prog = parse("flow F() -> Out { }");
10586        assert_eq!(prog.declarations.len(), 1);
10587        assert_eq!(prog.declaration_trivia.len(), 1);
10588        assert!(prog.declaration_trivia[0].leading.is_empty());
10589        assert!(prog.declaration_trivia[0].trailing.is_empty());
10590    }
10591
10592    #[test]
10593    fn doc_line_comment_attaches_as_leading() {
10594        let prog = parse("/// Documents F\nflow F() -> Out { }");
10595        let triv = &prog.declaration_trivia[0];
10596        assert_eq!(triv.leading.len(), 1);
10597        assert_eq!(triv.leading[0].kind, TriviaKind::DocLine);
10598        assert!(triv.leading[0].is_doc());
10599        assert_eq!(triv.leading[0].text, "/// Documents F");
10600    }
10601
10602    #[test]
10603    fn regular_line_comment_attaches_as_leading() {
10604        let prog = parse("// header\nflow F() -> Out { }");
10605        let triv = &prog.declaration_trivia[0];
10606        assert_eq!(triv.leading.len(), 1);
10607        assert_eq!(triv.leading[0].kind, TriviaKind::Line);
10608        assert!(!triv.leading[0].is_doc());
10609    }
10610
10611    #[test]
10612    fn block_doc_comment_attaches_as_leading() {
10613        let prog = parse("/** Doc block */\nflow F() -> Out { }");
10614        let triv = &prog.declaration_trivia[0];
10615        assert_eq!(triv.leading[0].kind, TriviaKind::DocBlock);
10616        assert!(triv.leading[0].is_doc());
10617    }
10618
10619    #[test]
10620    fn multiple_comments_collected_in_source_order() {
10621        let src = "/// First\n/// Second\nflow F() -> Out { }";
10622        let prog = parse(src);
10623        let triv = &prog.declaration_trivia[0];
10624        assert_eq!(triv.leading.len(), 2);
10625        assert_eq!(triv.leading[0].text, "/// First");
10626        assert_eq!(triv.leading[1].text, "/// Second");
10627    }
10628
10629    #[test]
10630    fn three_decls_each_get_own_leading() {
10631        let src = "/// for A\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }\n/// for C\nflow C() -> Out { }";
10632        let prog = parse(src);
10633        assert_eq!(prog.declarations.len(), 3);
10634        assert_eq!(prog.declaration_trivia.len(), 3);
10635        for (idx, name) in ["A", "B", "C"].iter().enumerate() {
10636            let triv = &prog.declaration_trivia[idx];
10637            assert_eq!(triv.leading.len(), 1);
10638            assert_eq!(triv.leading[0].text, format!("/// for {name}"));
10639        }
10640    }
10641
10642    #[test]
10643    fn trailing_comment_attaches_to_last_token_of_decl() {
10644        // Comment on the same line as the decl's closing brace.
10645        let prog = parse("flow F() -> Out { } // tail");
10646        let triv = &prog.declaration_trivia[0];
10647        assert_eq!(triv.trailing.len(), 1);
10648        assert_eq!(triv.trailing[0].text, "// tail");
10649    }
10650
10651    #[test]
10652    fn mixed_doc_and_regular_preserve_order_between_decls() {
10653        let src = "/// doc for A\nflow A() -> Out { }\n\n// header line\n/// doc for B\nflow B() -> Out { }";
10654        let prog = parse(src);
10655        assert_eq!(prog.declarations.len(), 2);
10656        // A: just the doc comment.
10657        assert_eq!(prog.declaration_trivia[0].leading.len(), 1);
10658        // B: header + doc, in source order.
10659        assert_eq!(prog.declaration_trivia[1].leading.len(), 2);
10660        assert_eq!(prog.declaration_trivia[1].leading[0].text, "// header line");
10661        assert_eq!(prog.declaration_trivia[1].leading[1].text, "/// doc for B");
10662    }
10663
10664    #[test]
10665    fn parser_unaffected_by_comments_in_grammar_path() {
10666        // The parser must accept comments interleaved between every
10667        // legal token without affecting the AST shape it produces.
10668        // This is the regression guard for "lossless lexing must not
10669        // change parsing semantics."
10670        let src =
10671            "// before flow\nflow /* between flow and name */ F() -> Out {\n  // body comment\n}";
10672        let prog = parse(src);
10673        assert_eq!(prog.declarations.len(), 1);
10674        if let Declaration::Flow(f) = &prog.declarations[0] {
10675            assert_eq!(f.name, "F");
10676        } else {
10677            panic!("expected Flow declaration");
10678        }
10679    }
10680}
10681
10682// ── §Fase 14.b — per-struct trivia fields tests ─────────────────────────────
10683//
10684// 14.b spreads `leading_trivia` / `trailing_trivia` into every Declaration
10685// variant struct (FlowDefinition, ChannelDefinition, PersonaDefinition, …).
10686// The Python AST already had this shape since 14.a; 14.b achieves Rust
10687// parity. The side-channel `Program.declaration_trivia` is preserved for
10688// backward compat — these tests verify the new direct access path.
10689
10690#[cfg(test)]
10691mod fase14b_per_struct_trivia_tests {
10692    use super::*;
10693    use crate::lexer::Lexer;
10694    use crate::tokens::TriviaKind;
10695
10696    fn parse(src: &str) -> Program {
10697        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
10698        Parser::new(toks).parse().expect("parse")
10699    }
10700
10701    #[test]
10702    fn flow_definition_carries_leading_trivia_directly() {
10703        let prog = parse("/// documents F\nflow F() -> Out { }");
10704        if let Declaration::Flow(f) = &prog.declarations[0] {
10705            assert_eq!(f.leading_trivia.len(), 1);
10706            assert_eq!(f.leading_trivia[0].kind, TriviaKind::DocLine);
10707            assert_eq!(f.leading_trivia[0].text, "/// documents F");
10708            assert!(f.trailing_trivia.is_empty());
10709        } else {
10710            panic!("expected Flow declaration");
10711        }
10712    }
10713
10714    #[test]
10715    fn flow_definition_carries_trailing_trivia_directly() {
10716        let prog = parse("flow F() -> Out { } // tail comment");
10717        if let Declaration::Flow(f) = &prog.declarations[0] {
10718            assert_eq!(f.trailing_trivia.len(), 1);
10719            assert_eq!(f.trailing_trivia[0].text, "// tail comment");
10720        } else {
10721            panic!("expected Flow declaration");
10722        }
10723    }
10724
10725    #[test]
10726    fn channel_definition_carries_trivia_directly() {
10727        // ChannelDefinition is a Tier-1 declaration; verify per-struct fields
10728        // populate just like FlowDefinition.
10729        let src = concat!(
10730            "/// inbound order events\n",
10731            "channel Orders {\n",
10732            "    message:     Order\n",
10733            "    qos:         at_least_once\n",
10734            "    lifetime:    affine\n",
10735            "    persistence: ephemeral\n",
10736            "    shield:      Broker\n",
10737            "}",
10738        );
10739        let prog = parse(src);
10740        if let Declaration::Channel(ch) = &prog.declarations[0] {
10741            assert_eq!(ch.leading_trivia.len(), 1);
10742            assert!(ch.leading_trivia[0].is_doc());
10743            assert_eq!(ch.leading_trivia[0].text, "/// inbound order events");
10744        } else {
10745            panic!("expected Channel declaration");
10746        }
10747    }
10748
10749    #[test]
10750    fn per_struct_fields_match_side_channel() {
10751        // 14.a side-channel and 14.b per-struct fields must hold identical
10752        // data — they are populated by the same parser pass.
10753        let src = "/// for A\n// header for B\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }";
10754        let prog = parse(src);
10755        for (idx, decl) in prog.declarations.iter().enumerate() {
10756            let side = &prog.declaration_trivia[idx];
10757            let (per_lead, per_trail) = match decl {
10758                Declaration::Flow(f) => (&f.leading_trivia, &f.trailing_trivia),
10759                _ => panic!("unexpected variant"),
10760            };
10761            assert_eq!(per_lead.len(), side.leading.len());
10762            assert_eq!(per_trail.len(), side.trailing.len());
10763            for (a, b) in per_lead.iter().zip(side.leading.iter()) {
10764                assert_eq!(a.text, b.text);
10765                assert_eq!(a.kind, b.kind);
10766            }
10767        }
10768    }
10769
10770    #[test]
10771    fn comment_free_program_yields_empty_per_struct_fields() {
10772        let prog = parse("flow F() -> Out { }");
10773        if let Declaration::Flow(f) = &prog.declarations[0] {
10774            assert!(f.leading_trivia.is_empty());
10775            assert!(f.trailing_trivia.is_empty());
10776        } else {
10777            panic!("expected Flow declaration");
10778        }
10779    }
10780}
10781
10782// ── §Fase 14.c — inner doc comments (//!, /*!) ──────────────────────────────
10783//
10784// Inner doc comments document the *enclosing* item rather than the next
10785// sibling. Today they flow through the trivia channel like any other
10786// comment; downstream consumers (axon doc, LSP) decide how to interpret
10787// `is_inner_doc()`. These tests verify the lexer→parser pipeline preserves
10788// the inner-doc discriminator end-to-end.
10789
10790#[cfg(test)]
10791mod fase14c_inner_doc_tests {
10792    use super::*;
10793    use crate::lexer::Lexer;
10794    use crate::tokens::TriviaKind;
10795
10796    fn parse(src: &str) -> Program {
10797        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
10798        Parser::new(toks).parse().expect("parse")
10799    }
10800
10801    #[test]
10802    fn inner_doc_line_reaches_leading_trivia() {
10803        let src = "//! file-level docs\nflow F() -> Out { }";
10804        let prog = parse(src);
10805        let triv = &prog.declaration_trivia[0];
10806        assert_eq!(triv.leading.len(), 1);
10807        assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocLine);
10808        assert!(triv.leading[0].is_doc());
10809        assert!(triv.leading[0].is_inner_doc());
10810        assert_eq!(triv.leading[0].text, "//! file-level docs");
10811        assert_eq!(triv.leading[0].stripped_text(), " file-level docs");
10812    }
10813
10814    #[test]
10815    fn inner_doc_block_reaches_leading_trivia() {
10816        let src = "/*! module-level docs */\nflow F() -> Out { }";
10817        let prog = parse(src);
10818        let triv = &prog.declaration_trivia[0];
10819        assert_eq!(triv.leading.len(), 1);
10820        assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocBlock);
10821        assert!(triv.leading[0].is_inner_doc());
10822        assert_eq!(triv.leading[0].stripped_text(), " module-level docs ");
10823    }
10824
10825    #[test]
10826    fn outer_and_inner_doc_can_coexist() {
10827        // File-level inner doc on top, then an outer doc for the
10828        // declaration. Both reach the trivia channel and remain
10829        // distinguishable via `is_inner_doc()`.
10830        let src = "//! file docs\n/// docs F\nflow F() -> Out { }";
10831        let prog = parse(src);
10832        let triv = &prog.declaration_trivia[0];
10833        assert_eq!(triv.leading.len(), 2);
10834        assert!(triv.leading[0].is_inner_doc());
10835        assert!(triv.leading[1].is_doc());
10836        assert!(!triv.leading[1].is_inner_doc());
10837    }
10838
10839    #[test]
10840    fn inner_doc_reaches_per_struct_fields() {
10841        // Same data must be visible via the per-struct fields (Fase 14.b).
10842        let src = "//! intro\nflow F() -> Out { }";
10843        let prog = parse(src);
10844        if let Declaration::Flow(f) = &prog.declarations[0] {
10845            assert_eq!(f.leading_trivia.len(), 1);
10846            assert!(f.leading_trivia[0].is_inner_doc());
10847        } else {
10848            panic!("expected Flow declaration");
10849        }
10850    }
10851}
10852
10853// ── §Fase 28.c — Parser error recovery test pack ─────────────────────────────
10854//
10855// Mirror of `tests/test_fase28_parser_recovery.py` (Python side, 28.b).
10856// The test classes here line up 1-1 with the Python ones so the cross-
10857// stack drift gate (28.i) can compare error-list shapes input-for-input.
10858//
10859// Test classes:
10860//   - backwards_compat: existing `parse()` API unchanged
10861//   - single_error_recovery: one bad decl → one error, rest parse OK
10862//   - multi_error_recovery: N independent errors → N entries
10863//   - sync_points: every top-level keyword resyncs correctly
10864//   - parse_result_api: `has_errors`, `is_clean`
10865//   - edge_cases: EOF mid-error, brace imbalance, only-bad-tokens
10866//   - robustness_fuzz: 1000 deterministic-seeded mutations never crash
10867//   - no_ghost_errors: single broken field produces exactly 1 error
10868//   - integration_with_colon_diagnostic: v1.19.4 hint preserved under
10869//     recovery mode
10870#[cfg(test)]
10871mod fase28_recovery_tests {
10872    use super::*;
10873    use crate::lexer::Lexer;
10874
10875    /// Lex a source and return tokens for the parser to consume.
10876    /// Mirrors the Python `_parse_recovery` helper.
10877    fn lex(src: &str) -> Vec<Token> {
10878        Lexer::new(src, "<test>").tokenize().expect("lex")
10879    }
10880
10881    /// Parse with recovery mode. Returns `(program, errors)` so call
10882    /// sites read like the Python helper.
10883    fn recover(src: &str) -> ParseResult {
10884        Parser::new(lex(src)).parse_with_recovery()
10885    }
10886
10887    /// Strict parse. Mirrors the Python `_parse_strict` helper.
10888    fn strict(src: &str) -> Result<Program, ParseError> {
10889        Parser::new(lex(src)).parse()
10890    }
10891
10892    // ── backwards_compat ─────────────────────────────────────────
10893
10894    #[test]
10895    fn strict_parse_unchanged_for_clean_source() {
10896        // The existing `parse()` API must continue to succeed
10897        // verbatim on every well-formed input — D9.
10898        let src = "intent I {}";
10899        let prog = strict(src).expect("clean parse");
10900        assert_eq!(prog.declarations.len(), 1);
10901    }
10902
10903    #[test]
10904    fn strict_parse_still_raises_on_first_error() {
10905        // D9 + D8: opt-in to recovery via `parse_with_recovery`;
10906        // strict mode must still bubble the first error.
10907        // (Using a parse-time error rather than a lex error — `@@@`
10908        // would be rejected by the lexer, which is out of scope.)
10909        let src = "flow F() { } not_a_keyword flow G() { }";
10910        let _ = strict(src).expect_err("must error fast in strict mode");
10911    }
10912
10913    #[test]
10914    fn recovery_clean_source_yields_no_errors() {
10915        let src = "flow F() { } flow G() { }";
10916        let pr = recover(src);
10917        assert!(pr.is_clean(), "errors: {:?}", pr.errors);
10918        assert_eq!(pr.program.declarations.len(), 2);
10919    }
10920
10921    // ── single_error_recovery ────────────────────────────────────
10922
10923    #[test]
10924    fn single_unknown_top_level_token_recovers() {
10925        // One garbage token at top level; rest must parse.
10926        let src = "garbage_token flow F() { } flow G() { }";
10927        let pr = recover(src);
10928        assert_eq!(pr.errors.len(), 1, "errors: {:?}", pr.errors);
10929        assert_eq!(pr.program.declarations.len(), 2);
10930    }
10931
10932    #[test]
10933    fn error_in_first_decl_does_not_block_second() {
10934        // `flow F` body refers to non-keyword `nope`; the error
10935        // recovery must skip to the next top-level keyword.
10936        let src = "flow F() { not_a_step nope } flow G() { }";
10937        let pr = recover(src);
10938        assert!(pr.has_errors(), "expected at least one error");
10939        // The second flow must be reachable.
10940        let names: Vec<&str> = pr
10941            .program
10942            .declarations
10943            .iter()
10944            .filter_map(|d| match d {
10945                Declaration::Flow(f) => Some(f.name.as_str()),
10946                _ => None,
10947            })
10948            .collect();
10949        assert!(names.contains(&"G"), "G not found among {names:?}");
10950    }
10951
10952    #[test]
10953    fn malformed_declaration_then_clean_intent_recovers() {
10954        let src = "flow @ () { } intent I {}";
10955        let pr = recover(src);
10956        assert!(pr.has_errors());
10957        let kinds: Vec<&str> = pr
10958            .program
10959            .declarations
10960            .iter()
10961            .map(|d| match d {
10962                Declaration::Intent(_) => "intent",
10963                Declaration::Flow(_) => "flow",
10964                _ => "other",
10965            })
10966            .collect();
10967        assert!(kinds.contains(&"intent"), "kinds: {kinds:?}");
10968    }
10969
10970    #[test]
10971    fn recovery_does_not_double_count_a_single_error() {
10972        // Regression for the "ghost error" pathology that surfaced
10973        // during 28.b dev: a nested-decl error must not also fire
10974        // an "Unexpected token at top level" from the outer loop.
10975        // The Rust grammar has stricter intra-flow requirements
10976        // than Python; the invariant we assert here is that the
10977        // outer loop emits zero "Unexpected token at top level"
10978        // errors after an inner step-shape error.
10979        let src = "flow F() { not_a_step }";
10980        let pr = recover(src);
10981        let outer_ghosts = pr
10982            .errors
10983            .iter()
10984            .filter(|e| e.message.contains("at top level"))
10985            .count();
10986        assert_eq!(outer_ghosts, 0, "ghost errors: {:?}", pr.errors);
10987    }
10988
10989    // ── multi_error_recovery ─────────────────────────────────────
10990
10991    #[test]
10992    fn three_independent_errors_yield_three_entries() {
10993        let src =
10994            "garbage1 flow F() { } garbage2 flow G() { } garbage3 flow H() { }";
10995        let pr = recover(src);
10996        assert_eq!(pr.errors.len(), 3, "errors: {:?}", pr.errors);
10997        assert_eq!(pr.program.declarations.len(), 3);
10998    }
10999
11000    #[test]
11001    fn all_errors_no_valid_declarations() {
11002        let src = "foo bar baz qux";
11003        let pr = recover(src);
11004        assert!(pr.has_errors());
11005        assert!(pr.program.declarations.is_empty());
11006    }
11007
11008    #[test]
11009    fn errors_recorded_in_source_order() {
11010        let src = "x flow A() { } y flow B() { } z flow C() { }";
11011        let pr = recover(src);
11012        assert_eq!(pr.errors.len(), 3);
11013        let lines: Vec<u32> = pr.errors.iter().map(|e| e.line).collect();
11014        // Same source-line means we compare by column ordering;
11015        // either way they must be non-decreasing.
11016        assert!(
11017            lines.windows(2).all(|w| w[0] <= w[1]),
11018            "errors out of order: {lines:?}"
11019        );
11020    }
11021
11022    // ── sync_points ──────────────────────────────────────────────
11023
11024    #[test]
11025    fn sync_to_flow_keyword() {
11026        let src = "garbage flow F() { }";
11027        let pr = recover(src);
11028        assert_eq!(pr.program.declarations.len(), 1);
11029    }
11030
11031    #[test]
11032    fn sync_to_intent_keyword() {
11033        let src = "garbage intent I {}";
11034        let pr = recover(src);
11035        assert_eq!(pr.program.declarations.len(), 1);
11036    }
11037
11038    #[test]
11039    fn sync_to_persona_keyword() {
11040        let src = "garbage persona P { name: \"P\" role: \"R\" }";
11041        let pr = recover(src);
11042        assert!(
11043            pr.program
11044                .declarations
11045                .iter()
11046                .any(|d| matches!(d, Declaration::Persona(_))),
11047            "persona not recovered: decls = {:?}",
11048            pr.program.declarations.len()
11049        );
11050    }
11051
11052    #[test]
11053    fn sync_to_run_keyword() {
11054        let src = "garbage run R { input: { user_message: \"hi\" } }";
11055        let pr = recover(src);
11056        // Either Run was parsed, or recovery still produced ≥1 err.
11057        assert!(pr.has_errors());
11058    }
11059
11060    // ── parse_result_api ─────────────────────────────────────────
11061
11062    #[test]
11063    fn parse_result_has_errors_and_is_clean_invert() {
11064        let pr_clean = recover("flow F() { }");
11065        assert!(pr_clean.is_clean());
11066        assert!(!pr_clean.has_errors());
11067
11068        let pr_err = recover("garbage");
11069        assert!(!pr_err.is_clean());
11070        assert!(pr_err.has_errors());
11071    }
11072
11073    #[test]
11074    fn parse_result_program_field_holds_partial_program() {
11075        let pr = recover("garbage flow F() { }");
11076        assert!(!pr.program.declarations.is_empty());
11077    }
11078
11079    #[test]
11080    fn parse_result_errors_carry_line_and_column() {
11081        let pr = recover("garbage");
11082        assert!(!pr.errors.is_empty());
11083        let e = &pr.errors[0];
11084        assert!(e.line >= 1);
11085        // Column may be 0-based or 1-based depending on lexer;
11086        // accept anything ≥ 0.
11087        let _ = e.column;
11088        assert!(!e.message.is_empty());
11089    }
11090
11091    #[test]
11092    fn parse_result_debug_renders() {
11093        let pr = recover("flow F() { }");
11094        let s = format!("{pr:?}");
11095        assert!(s.contains("ParseResult"));
11096    }
11097
11098    // ── edge_cases ───────────────────────────────────────────────
11099
11100    #[test]
11101    fn empty_source_is_clean() {
11102        let pr = recover("");
11103        assert!(pr.is_clean());
11104        assert!(pr.program.declarations.is_empty());
11105    }
11106
11107    #[test]
11108    fn whitespace_only_source_is_clean() {
11109        let pr = recover("   \n\n\t  \n");
11110        assert!(pr.is_clean());
11111        assert!(pr.program.declarations.is_empty());
11112    }
11113
11114    #[test]
11115    fn only_garbage_does_not_crash() {
11116        // Lex-clean garbage tokens (avoids AxonLexerError).
11117        let pr = recover("foo bar baz { qux quux } corge { grault }");
11118        assert!(pr.has_errors());
11119    }
11120
11121    #[test]
11122    fn unbalanced_close_brace_does_not_crash() {
11123        let pr = recover("} flow F() { }");
11124        // Recovery must keep walking past stray `}`.
11125        let names: Vec<&str> = pr
11126            .program
11127            .declarations
11128            .iter()
11129            .filter_map(|d| match d {
11130                Declaration::Flow(f) => Some(f.name.as_str()),
11131                _ => None,
11132            })
11133            .collect();
11134        assert!(names.contains(&"F"), "F not recovered: {names:?}");
11135    }
11136
11137    #[test]
11138    fn error_at_eof_does_not_loop() {
11139        // Truncated declaration. Must terminate; finite errors.
11140        let pr = recover("flow F() { ");
11141        // Either errored or somehow accepted — but must terminate.
11142        let _ = pr.errors.len();
11143    }
11144
11145    #[test]
11146    fn nested_braces_inside_error_still_balance() {
11147        // Walker must respect brace depth so a `}` inside a malformed
11148        // block does not prematurely sync.
11149        let src = "flow F() { not_a_step { inner } } flow G() { }";
11150        let pr = recover(src);
11151        let names: Vec<&str> = pr
11152            .program
11153            .declarations
11154            .iter()
11155            .filter_map(|d| match d {
11156                Declaration::Flow(f) => Some(f.name.as_str()),
11157                _ => None,
11158            })
11159            .collect();
11160        assert!(names.contains(&"G"), "G not recovered: {names:?}");
11161    }
11162
11163    // ── robustness_fuzz ──────────────────────────────────────────
11164    //
11165    // Deterministic-seeded mutator (xorshift). 100 buckets ×
11166    // 10 mutations = 1000 iterations, byte-bounded so fuzz time
11167    // stays under 1 s on a release build. Recovery must NEVER crash;
11168    // lexer-level errors are out of scope (lexer recovery is its own
11169    // sub-fase). 28.b mirrors this with the same structure.
11170
11171    #[derive(Clone, Copy)]
11172    struct Xorshift(u64);
11173    impl Xorshift {
11174        fn next(&mut self) -> u64 {
11175            let mut x = self.0;
11176            x ^= x << 13;
11177            x ^= x >> 7;
11178            x ^= x << 17;
11179            self.0 = x;
11180            x
11181        }
11182        fn pick<T: Copy>(&mut self, slice: &[T]) -> T {
11183            slice[(self.next() as usize) % slice.len()]
11184        }
11185    }
11186
11187    fn mutate(src: &str, rng: &mut Xorshift) -> String {
11188        let mut bytes: Vec<u8> = src.bytes().collect();
11189        if bytes.is_empty() {
11190            return src.to_string();
11191        }
11192        let op = rng.next() % 4;
11193        let pos = (rng.next() as usize) % bytes.len();
11194        // Stick to ASCII-safe printable bytes to keep input lex-able
11195        // most of the time. AxonLexerError is still possible and is
11196        // tolerated by the recovery contract.
11197        let safe: &[u8] = b"abcdefghijklmnopqrstuvwxyz {}();:,_0123456789";
11198        match op {
11199            0 => {
11200                bytes.remove(pos);
11201            }
11202            1 => {
11203                let b = rng.pick(safe);
11204                bytes.insert(pos, b);
11205            }
11206            2 if pos + 1 < bytes.len() => {
11207                bytes.swap(pos, pos + 1);
11208            }
11209            _ => {
11210                let b = rng.pick(safe);
11211                bytes[pos] = b;
11212            }
11213        }
11214        // Lossy decode: mutator may have produced invalid UTF-8;
11215        // strip non-ASCII before handing to the lexer.
11216        bytes.retain(|b| b.is_ascii());
11217        String::from_utf8_lossy(&bytes).into_owned()
11218    }
11219
11220    #[test]
11221    fn fuzz_recovery_never_crashes() {
11222        let seed_bases = [
11223            "flow F() { }",
11224            "intent I { }",
11225            "persona P { name: \"P\" role: \"R\" }",
11226            "intent J { ask: \"a\" }",
11227            "type T = String",
11228        ];
11229        // 100 buckets × 10 mutations = 1000 iterations, deterministic.
11230        for (bucket, base) in (0..100u64).zip(seed_bases.iter().cycle()) {
11231            let mut rng = Xorshift(0x1234_5678_9abc_def0_u64.wrapping_add(bucket));
11232            let mut current = (*base).to_string();
11233            for _ in 0..10 {
11234                current = mutate(&current, &mut rng);
11235                // Lexer may reject; that's outside parser-recovery
11236                // scope (28.b/c). Skip those iterations.
11237                let toks = match Lexer::new(&current, "<fuzz>").tokenize() {
11238                    Ok(t) => t,
11239                    Err(_) => continue,
11240                };
11241                // Recovery must not panic on any well-lexed input.
11242                let _pr = Parser::new(toks).parse_with_recovery();
11243            }
11244        }
11245    }
11246
11247    // ── integration_with_v1_19_4_colon_diagnostic ────────────────
11248
11249    #[test]
11250    fn missing_colon_hint_preserved_under_recovery() {
11251        // The Rust frontend's strict `parse()` carries the same
11252        // colon diagnostic shape as the Python side. Recovery mode
11253        // must not erase it.
11254        let src = "flow F() { run R { input { user_message: \"hi\" } } }";
11255        let pr = recover(src);
11256        // Either the parser accepts this (some shape may be valid)
11257        // or it errors — but if it errors, the message must surface
11258        // the diagnostic content.
11259        if !pr.errors.is_empty() {
11260            let any_msg = pr.errors.iter().any(|e| !e.message.is_empty());
11261            assert!(any_msg);
11262        }
11263    }
11264
11265    // ── recovery preserves declaration ordering ──────────────────
11266
11267    #[test]
11268    fn recovered_declarations_appear_in_source_order() {
11269        let src = "flow A() { } garbage flow B() { } garbage flow C() { }";
11270        let pr = recover(src);
11271        let names: Vec<&str> = pr
11272            .program
11273            .declarations
11274            .iter()
11275            .filter_map(|d| match d {
11276                Declaration::Flow(f) => Some(f.name.as_str()),
11277                _ => None,
11278            })
11279            .collect();
11280        assert_eq!(names, vec!["A", "B", "C"]);
11281    }
11282}
11283
11284// ── §Fase 28.d — Source-context diagnostic block test pack ───────────────────
11285//
11286// Mirror of `tests/test_fase28_source_context.py` (Python side, 28.d).
11287// The render output must be byte-identical to the Python `SourceSnippet.render`
11288// on the same input — D7 ratified (cross-stack drift gate). Golden strings
11289// in `golden_*` tests are duplicated verbatim in the Python pack; edits
11290// here MUST be mirrored on the Python side and vice versa.
11291#[cfg(test)]
11292mod fase28_source_context_tests {
11293    use super::*;
11294    use crate::lexer::Lexer;
11295
11296    fn snippet(source: &str, line: u32, column: u32, filename: &str) -> String {
11297        SourceSnippet::new(
11298            source.to_string(),
11299            line,
11300            column,
11301            filename.to_string(),
11302        )
11303        .render()
11304    }
11305
11306    // ── Pure rendering ──────────────────────────────────────────
11307
11308    #[test]
11309    fn rustc_style_block_for_middle_line() {
11310        let src = "line one\nline two\nline three\nline four\nline five";
11311        let out = snippet(src, 3, 6, "x.axon");
11312        assert!(out.contains("--> x.axon:3:6"));
11313        assert!(out.contains("1 | line one"));
11314        assert!(out.contains("2 | line two"));
11315        assert!(out.contains("3 | line three"));
11316        assert!(out.contains("4 | line four"));
11317        assert!(out.contains("5 | line five"));
11318        // Caret col 6 → 5-space pad. Empty gutter is 1 space (gutter=1).
11319        assert!(out.contains("\n  |      ^"), "out:\n{out}");
11320    }
11321
11322    #[test]
11323    fn caret_column_one_renders_correctly() {
11324        let out = snippet("abc\n", 1, 1, "<source>");
11325        assert!(out.contains("\n  | ^"));
11326    }
11327
11328    #[test]
11329    fn first_line_clamps_context_before_to_zero() {
11330        let src = "first\nsecond\nthird\nfourth\nfifth";
11331        let out = snippet(src, 1, 1, "<source>");
11332        assert!(out.contains("1 | first"));
11333        assert!(out.contains("2 | second"));
11334        assert!(out.contains("3 | third"));
11335        assert!(!out.contains("4 | fourth"));
11336    }
11337
11338    #[test]
11339    fn last_line_clamps_context_after_to_eof() {
11340        let src = "first\nsecond\nthird\nfourth\nfifth";
11341        let out = snippet(src, 5, 2, "<source>");
11342        assert!(out.contains("5 | fifth"));
11343        assert!(out.contains("3 | third"));
11344        assert!(out.contains("4 | fourth"));
11345        assert!(!out.contains("2 | second"));
11346    }
11347
11348    #[test]
11349    fn gutter_width_grows_with_line_count() {
11350        let src: String = (1..=12).map(|i| format!("line{i}")).collect::<Vec<_>>().join("\n");
11351        let out = snippet(&src, 12, 1, "<source>");
11352        assert!(out.contains("12 | line12"));
11353        assert!(out.contains("10 | line10"));
11354    }
11355
11356    // ── Edge cases ──────────────────────────────────────────────
11357
11358    #[test]
11359    fn empty_source_returns_empty() {
11360        assert_eq!(snippet("", 1, 1, "<source>"), "");
11361    }
11362
11363    #[test]
11364    fn zero_line_returns_empty() {
11365        assert_eq!(snippet("hi", 0, 1, "<source>"), "");
11366    }
11367
11368    #[test]
11369    fn out_of_range_line_returns_empty() {
11370        assert_eq!(snippet("hi", 99, 1, "<source>"), "");
11371    }
11372
11373    #[test]
11374    fn caret_clamps_past_eol() {
11375        let out = snippet("hello", 1, 50, "<source>");
11376        assert!(out.contains("\n  |      ^"), "out:\n{out}");
11377    }
11378
11379    #[test]
11380    fn unicode_codepoint_count_for_caret_clamp() {
11381        // "héllo" = 5 codepoints; column past EOL clamps to 6.
11382        let out = snippet("héllo", 1, 99, "<source>");
11383        assert!(out.contains("\n  |      ^"), "out:\n{out}");
11384    }
11385
11386    #[test]
11387    fn trailing_newline_does_not_create_phantom_last_line() {
11388        let out = snippet("first\nsecond\n", 2, 1, "<source>");
11389        assert!(!out.contains("3 |"));
11390        assert!(out.contains("2 | second"));
11391    }
11392
11393    // ── Parser attach plumbing ──────────────────────────────────
11394
11395    fn lex(src: &str) -> Vec<Token> {
11396        Lexer::new(src, "<test>").tokenize().expect("lex")
11397    }
11398
11399    #[test]
11400    fn strict_parse_attaches_snippet_when_source_given() {
11401        let src = "garbage_token\nflow F() { }";
11402        let err = Parser::new(lex(src))
11403            .with_source(src, "x.axon")
11404            .parse()
11405            .expect_err("must error");
11406        assert!(err.source_snippet.is_some());
11407        let display = format!("{err}");
11408        assert!(display.contains("--> x.axon:"), "display: {display}");
11409    }
11410
11411    #[test]
11412    fn strict_parse_no_snippet_when_no_source() {
11413        let src = "garbage_token";
11414        let err = Parser::new(lex(src)).parse().expect_err("must error");
11415        assert!(err.source_snippet.is_none());
11416        let display = format!("{err}");
11417        assert!(!display.contains("\n  -->"));
11418    }
11419
11420    #[test]
11421    fn every_recovered_error_has_snippet() {
11422        let src = "garbage1\nflow F() { }\ngarbage2\nflow G() { }";
11423        let result = Parser::new(lex(src))
11424            .with_source(src, "multi.axon")
11425            .parse_with_recovery();
11426        assert!(!result.errors.is_empty());
11427        for err in &result.errors {
11428            assert!(err.source_snippet.is_some());
11429            let display = format!("{err}");
11430            assert!(
11431                display.contains("--> multi.axon:"),
11432                "display: {display}"
11433            );
11434        }
11435    }
11436
11437    #[test]
11438    fn recovery_no_snippet_when_no_source() {
11439        let src = "garbage1 garbage2";
11440        let result = Parser::new(lex(src)).parse_with_recovery();
11441        for err in &result.errors {
11442            assert!(err.source_snippet.is_none());
11443        }
11444    }
11445
11446    #[test]
11447    fn snippet_points_at_correct_line_for_each_error() {
11448        let src = "garbage_a\nflow F() { }\ngarbage_b\nflow G() { }";
11449        let result = Parser::new(lex(src))
11450            .with_source(src, "x")
11451            .parse_with_recovery();
11452        for err in &result.errors {
11453            let sn = err.source_snippet.as_ref().expect("snippet");
11454            assert_eq!(sn.line, err.line);
11455        }
11456    }
11457
11458    // ── Backwards-compat ────────────────────────────────────────
11459
11460    #[test]
11461    fn legacy_constructor_still_works() {
11462        let src = "flow F() { }";
11463        let prog = Parser::new(lex(src)).parse().expect("clean");
11464        assert_eq!(prog.declarations.len(), 1);
11465    }
11466
11467    #[test]
11468    fn attach_source_idempotent() {
11469        let err = ParseError {
11470            message: "bad".to_string(),
11471            line: 2,
11472            column: 3,
11473            ..Default::default()
11474        };
11475        let err2 = err.clone().attach_source("a\nb\nc\n", "f.axon");
11476        let first = format!("{err2}");
11477        let err3 = err.attach_source("a\nb\nc\n", "f.axon");
11478        let second = format!("{err3}");
11479        assert_eq!(first, second);
11480    }
11481
11482    #[test]
11483    fn attach_source_noop_when_line_zero() {
11484        let err = ParseError {
11485            message: "bad".to_string(),
11486            line: 0,
11487            column: 0,
11488            ..Default::default()
11489        };
11490        let err = err.attach_source("a\nb\nc\n", "f.axon");
11491        assert!(err.source_snippet.is_none());
11492    }
11493
11494    // ── Cross-stack golden parity ───────────────────────────────
11495    // These golden strings are duplicated verbatim in the Python
11496    // test pack at `tests/test_fase28_source_context.py::TestRustParityShape`.
11497    // Edits here MUST be mirrored in the Python pack — D7.
11498
11499    #[test]
11500    fn golden_simple_three_line_block() {
11501        let src = "alpha\nbeta\ngamma";
11502        let out = snippet(src, 2, 3, "g.axon");
11503        // Note: gutter=1, so empty_gutter=" " (one space). The
11504        // " --> ..." line therefore starts with two spaces ("<empty>"
11505        // + literal " --> ...").
11506        let expected = concat!(
11507            "  --> g.axon:2:3\n",
11508            "  |\n",
11509            "1 | alpha\n",
11510            "2 | beta\n",
11511            "  |   ^\n",
11512            "3 | gamma",
11513        );
11514        assert_eq!(out, expected);
11515    }
11516
11517    #[test]
11518    fn golden_first_line_caret() {
11519        let src = "abc\ndef\n";
11520        let out = snippet(src, 1, 1, "x");
11521        let expected = concat!(
11522            "  --> x:1:1\n",
11523            "  |\n",
11524            "1 | abc\n",
11525            "  | ^\n",
11526            "2 | def",
11527        );
11528        assert_eq!(out, expected);
11529    }
11530
11531    #[test]
11532    fn golden_two_digit_gutter() {
11533        let src: String = (1..=11)
11534            .map(|i| format!("L{i}"))
11535            .collect::<Vec<_>>()
11536            .join("\n");
11537        let out = snippet(&src, 10, 2, "big");
11538        let expected = concat!(
11539            "   --> big:10:2\n",
11540            "   |\n",
11541            " 8 | L8\n",
11542            " 9 | L9\n",
11543            "10 | L10\n",
11544            "   |  ^\n",
11545            "11 | L11",
11546        );
11547        assert_eq!(out, expected);
11548    }
11549}
11550
11551// ── §Fase 28.e — Parser integration tests for smart-suggest ──────────────────
11552//
11553// Mirror of `tests/test_fase28_smart_suggest.py::TestParserIntegration`.
11554// Verifies that the parser actually wires `suggest_for` into the
11555// unknown-keyword diagnostic at both error sites — top-level and
11556// flow-body.
11557#[cfg(test)]
11558mod fase28_smart_suggest_parser_tests {
11559    use super::*;
11560    use crate::lexer::Lexer;
11561
11562    fn lex(src: &str) -> Vec<Token> {
11563        Lexer::new(src, "<test>").tokenize().expect("lex")
11564    }
11565
11566    #[test]
11567    fn top_level_typo_suggests_flow() {
11568        let src = "flwo F() { }";
11569        let err = Parser::new(lex(src)).parse().expect_err("must error");
11570        assert!(
11571            err.message.contains("Did you mean `flow`?"),
11572            "msg: {}",
11573            err.message
11574        );
11575    }
11576
11577    #[test]
11578    fn top_level_unknown_far_no_suggestion() {
11579        let src = "qwerty F() { }";
11580        let err = Parser::new(lex(src)).parse().expect_err("must error");
11581        assert!(
11582            !err.message.contains("Did you mean"),
11583            "msg: {}",
11584            err.message
11585        );
11586    }
11587
11588    #[test]
11589    fn flow_body_typo_suggests_step() {
11590        let src = "flow F() { stepp S {} }";
11591        let err = Parser::new(lex(src)).parse().expect_err("must error");
11592        assert!(
11593            err.message.contains("Did you mean `step`"),
11594            "msg: {}",
11595            err.message
11596        );
11597    }
11598
11599    #[test]
11600    fn flow_body_typo_suggests_reason() {
11601        let src = "flow F() { reasn R {} }";
11602        let err = Parser::new(lex(src)).parse().expect_err("must error");
11603        assert!(
11604            err.message.contains("Did you mean `reason`?"),
11605            "msg: {}",
11606            err.message
11607        );
11608    }
11609
11610    #[test]
11611    fn recovery_mode_carries_hint() {
11612        let src = "flwo F() { }";
11613        let result = Parser::new(lex(src)).parse_with_recovery();
11614        assert!(
11615            result
11616                .errors
11617                .iter()
11618                .any(|e| e.message.contains("Did you mean `flow`?")),
11619            "errors: {:?}",
11620            result.errors
11621        );
11622    }
11623}
11624
11625// ── §Fase 35.m — mutate / purge where-clause capture ────────────────
11626
11627#[cfg(test)]
11628mod fase35m_mutate_purge_where_tests {
11629    use super::*;
11630
11631    fn parse(src: &str) -> Program {
11632        let tokens = crate::lexer::Lexer::new(src, "<test>")
11633            .tokenize()
11634            .expect("lex");
11635        Parser::new(tokens).parse().expect("parse")
11636    }
11637
11638    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
11639        for d in &prog.declarations {
11640            if let Declaration::Flow(f) = d {
11641                if f.name == flow {
11642                    return f.body.first().expect("flow has at least one step");
11643                }
11644            }
11645        }
11646        panic!("flow `{flow}` not found");
11647    }
11648
11649    #[test]
11650    fn mutate_captures_its_where_clause() {
11651        // Pre-35.m the `{ where: }` block was skipped — every mutate
11652        // ran whole-store. It must now reach `where_expr`.
11653        let prog =
11654            parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
11655        match first_step(&prog, "F") {
11656            FlowStep::Mutate(m) => {
11657                assert_eq!(m.store_name, "accounts");
11658                assert_eq!(m.where_expr, "id = 1");
11659            }
11660            other => panic!("expected Mutate, got {other:?}"),
11661        }
11662    }
11663
11664    #[test]
11665    fn purge_captures_its_where_clause() {
11666        let prog =
11667            parse("flow F() -> Unit { purge logs { where: \"ts < 100\" } }");
11668        match first_step(&prog, "F") {
11669            FlowStep::Purge(p) => {
11670                assert_eq!(p.store_name, "logs");
11671                assert_eq!(p.where_expr, "ts < 100");
11672            }
11673            other => panic!("expected Purge, got {other:?}"),
11674        }
11675    }
11676
11677    #[test]
11678    fn mutate_without_a_where_block_is_a_whole_store_op() {
11679        // No `{ where: }` → an empty filter → the runtime renders
11680        // `WHERE TRUE` (every row). A valid, intentional op.
11681        let prog = parse("flow F() -> Unit { mutate accounts }");
11682        match first_step(&prog, "F") {
11683            FlowStep::Mutate(m) => {
11684                assert_eq!(m.store_name, "accounts");
11685                assert_eq!(m.where_expr, "");
11686            }
11687            other => panic!("expected Mutate, got {other:?}"),
11688        }
11689    }
11690}
11691
11692// ── §Fase 35.o — persist field-block capture ────────────────────────
11693
11694#[cfg(test)]
11695mod fase35o_persist_fields_tests {
11696    use super::*;
11697
11698    fn parse(src: &str) -> Program {
11699        let tokens = crate::lexer::Lexer::new(src, "<test>")
11700            .tokenize()
11701            .expect("lex");
11702        Parser::new(tokens).parse().expect("parse")
11703    }
11704
11705    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
11706        for d in &prog.declarations {
11707            if let Declaration::Flow(f) = d {
11708                if f.name == flow {
11709                    return f.body.first().expect("flow has at least one step");
11710                }
11711            }
11712        }
11713        panic!("flow `{flow}` not found");
11714    }
11715
11716    #[test]
11717    fn persist_captures_its_field_block() {
11718        // Pre-35.o the `{ col: value }` block was skipped — every
11719        // persist wrote the whole binding context. It must now reach
11720        // `fields`, in source order, with value expressions raw.
11721        let prog = parse(
11722            "flow F() -> Unit { persist into chat_history { \
11723             session_id: \"${session_id}\" sender: \"user\" \
11724             content: \"${message}\" } }",
11725        );
11726        match first_step(&prog, "F") {
11727            FlowStep::Persist(p) => {
11728                assert_eq!(p.store_name, "chat_history");
11729                assert_eq!(
11730                    p.fields,
11731                    vec![
11732                        ("session_id".to_string(), "${session_id}".to_string()),
11733                        ("sender".to_string(), "user".to_string()),
11734                        ("content".to_string(), "${message}".to_string()),
11735                    ]
11736                );
11737            }
11738            other => panic!("expected Persist, got {other:?}"),
11739        }
11740    }
11741
11742    #[test]
11743    fn persist_without_a_block_keeps_the_user_bindings_fallback() {
11744        // No `{ }` → empty `fields` → the runtime falls back to the
11745        // v1.30.0 user-bindings row. Backward-compatible.
11746        let prog = parse("flow F() -> Unit { persist events }");
11747        match first_step(&prog, "F") {
11748            FlowStep::Persist(p) => {
11749                assert_eq!(p.store_name, "events");
11750                assert!(p.fields.is_empty());
11751            }
11752            other => panic!("expected Persist, got {other:?}"),
11753        }
11754    }
11755
11756    #[test]
11757    fn persist_accepts_the_optional_into_connector() {
11758        // `persist into X` and `persist X` resolve to the SAME store
11759        // name — pre-35.o `into` was captured AS the store name.
11760        let with =
11761            parse("flow F() -> Unit { persist into accounts { id: \"1\" } }");
11762        let without =
11763            parse("flow F() -> Unit { persist accounts { id: \"1\" } }");
11764        for prog in [&with, &without] {
11765            match first_step(prog, "F") {
11766                FlowStep::Persist(p) => assert_eq!(p.store_name, "accounts"),
11767                other => panic!("expected Persist, got {other:?}"),
11768            }
11769        }
11770    }
11771
11772    #[test]
11773    fn persist_into_without_a_block_resolves_the_store_name() {
11774        // `persist into events` — the `into` connector is skipped, the
11775        // store name is `events` (not `into`). Lateral bug closed.
11776        let prog = parse("flow F() -> Unit { persist into events }");
11777        match first_step(&prog, "F") {
11778            FlowStep::Persist(p) => {
11779                assert_eq!(p.store_name, "events");
11780                assert!(p.fields.is_empty());
11781            }
11782            other => panic!("expected Persist, got {other:?}"),
11783        }
11784    }
11785
11786    #[test]
11787    fn persist_fields_lower_into_the_ir() {
11788        // The IR generator must carry `fields` onto `IRPersistStep`
11789        // so the runtime reads exactly the declared columns.
11790        let prog = parse(
11791            "flow F() -> Unit { persist into chat { content: \"${msg}\" } }",
11792        );
11793        let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
11794        let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
11795        match flow.steps.first().expect("one step") {
11796            crate::ir_nodes::IRFlowNode::Persist(p) => {
11797                assert_eq!(p.store_name, "chat");
11798                assert_eq!(
11799                    p.fields,
11800                    vec![("content".to_string(), "${msg}".to_string())]
11801                );
11802            }
11803            other => panic!("expected IRFlowNode::Persist, got {other:?}"),
11804        }
11805    }
11806}
11807
11808// ── §Fase 35.p — mutate SET-field-block capture ─────────────────────
11809
11810#[cfg(test)]
11811mod fase35p_mutate_fields_tests {
11812    use super::*;
11813
11814    fn parse(src: &str) -> Program {
11815        let tokens = crate::lexer::Lexer::new(src, "<test>")
11816            .tokenize()
11817            .expect("lex");
11818        Parser::new(tokens).parse().expect("parse")
11819    }
11820
11821    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
11822        for d in &prog.declarations {
11823            if let Declaration::Flow(f) = d {
11824                if f.name == flow {
11825                    return f.body.first().expect("flow has at least one step");
11826                }
11827            }
11828        }
11829        panic!("flow `{flow}` not found");
11830    }
11831
11832    #[test]
11833    fn mutate_captures_its_set_field_block() {
11834        // Pre-35.p every key but `where:` was skipped — the runtime
11835        // SET every flow binding. The SET columns must now reach
11836        // `fields`, in source order, with `where:` still captured.
11837        let prog = parse(
11838            "flow F() -> Unit { mutate accounts { where: \"id = ${id}\" \
11839             balance: \"${new_balance}\" status: \"active\" } }",
11840        );
11841        match first_step(&prog, "F") {
11842            FlowStep::Mutate(m) => {
11843                assert_eq!(m.store_name, "accounts");
11844                assert_eq!(m.where_expr, "id = ${id}");
11845                assert_eq!(
11846                    m.fields,
11847                    vec![
11848                        ("balance".to_string(), "${new_balance}".to_string()),
11849                        ("status".to_string(), "active".to_string()),
11850                    ]
11851                );
11852            }
11853            other => panic!("expected Mutate, got {other:?}"),
11854        }
11855    }
11856
11857    #[test]
11858    fn mutate_where_only_block_has_no_set_fields() {
11859        // A `{ where: }`-only block → empty `fields` → the runtime
11860        // falls back to the v1.31.0 user-bindings SET.
11861        let prog =
11862            parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
11863        match first_step(&prog, "F") {
11864            FlowStep::Mutate(m) => {
11865                assert_eq!(m.where_expr, "id = 1");
11866                assert!(m.fields.is_empty());
11867            }
11868            other => panic!("expected Mutate, got {other:?}"),
11869        }
11870    }
11871
11872    #[test]
11873    fn mutate_with_no_block_is_a_whole_store_op() {
11874        // No block at all → empty where + empty fields (a whole-store
11875        // UPDATE from user bindings) — unchanged from 35.m.
11876        let prog = parse("flow F() -> Unit { mutate accounts }");
11877        match first_step(&prog, "F") {
11878            FlowStep::Mutate(m) => {
11879                assert_eq!(m.store_name, "accounts");
11880                assert_eq!(m.where_expr, "");
11881                assert!(m.fields.is_empty());
11882            }
11883            other => panic!("expected Mutate, got {other:?}"),
11884        }
11885    }
11886
11887    #[test]
11888    fn mutate_fields_lower_into_the_ir() {
11889        let prog = parse(
11890            "flow F() -> Unit { mutate t { where: \"id = 1\" v: \"${x}\" } }",
11891        );
11892        let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
11893        let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
11894        match flow.steps.first().expect("one step") {
11895            crate::ir_nodes::IRFlowNode::Mutate(m) => {
11896                assert_eq!(m.where_expr, "id = 1");
11897                assert_eq!(
11898                    m.fields,
11899                    vec![("v".to_string(), "${x}".to_string())]
11900                );
11901            }
11902            other => panic!("expected IRFlowNode::Mutate, got {other:?}"),
11903        }
11904    }
11905}
11906