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 (v1.5.2). 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/// v1.5.2 — 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        // v2.69.0 — 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        // v2.87.0 — an `effect` declaration carries its comments like any peer.
80        Declaration::Effect(n) => {
81            n.leading_trivia = leading;
82            n.trailing_trivia = trailing;
83        }
84        Declaration::Tool(n) => {
85            n.leading_trivia = leading;
86            n.trailing_trivia = trailing;
87        }
88        Declaration::Type(n) => {
89            n.leading_trivia = leading;
90            n.trailing_trivia = trailing;
91        }
92        Declaration::Flow(n) => {
93            n.leading_trivia = leading;
94            n.trailing_trivia = trailing;
95        }
96        Declaration::Intent(n) => {
97            n.leading_trivia = leading;
98            n.trailing_trivia = trailing;
99        }
100        Declaration::Run(n) => {
101            n.leading_trivia = leading;
102            n.trailing_trivia = trailing;
103        }
104        Declaration::Epistemic(n) => {
105            n.leading_trivia = leading;
106            n.trailing_trivia = trailing;
107        }
108        Declaration::Let(n) => {
109            n.leading_trivia = leading;
110            n.trailing_trivia = trailing;
111        }
112        Declaration::LambdaData(n) => {
113            n.leading_trivia = leading;
114            n.trailing_trivia = trailing;
115        }
116        Declaration::Agent(n) => {
117            n.leading_trivia = leading;
118            n.trailing_trivia = trailing;
119        }
120        Declaration::Shield(n) => {
121            n.leading_trivia = leading;
122            n.trailing_trivia = trailing;
123        }
124        Declaration::Window(n) => {
125            n.leading_trivia = leading;
126            n.trailing_trivia = trailing;
127        }
128        Declaration::Pix(n) => {
129            n.leading_trivia = leading;
130            n.trailing_trivia = trailing;
131        }
132        Declaration::Ledger(n) => {
133            n.leading_trivia = leading;
134            n.trailing_trivia = trailing;
135        }
136        Declaration::Psyche(n) => {
137            n.leading_trivia = leading;
138            n.trailing_trivia = trailing;
139        }
140        Declaration::Corpus(n) => {
141            n.leading_trivia = leading;
142            n.trailing_trivia = trailing;
143        }
144        Declaration::Dataspace(n) => {
145            n.leading_trivia = leading;
146            n.trailing_trivia = trailing;
147        }
148        Declaration::Ots(n) => {
149            n.leading_trivia = leading;
150            n.trailing_trivia = trailing;
151        }
152        Declaration::Mandate(n) => {
153            n.leading_trivia = leading;
154            n.trailing_trivia = trailing;
155        }
156        Declaration::Compute(n) => {
157            n.leading_trivia = leading;
158            n.trailing_trivia = trailing;
159        }
160        Declaration::Daemon(n) => {
161            n.leading_trivia = leading;
162            n.trailing_trivia = trailing;
163        }
164        Declaration::Extension(n) => {
165            n.leading_trivia = leading;
166            n.trailing_trivia = trailing;
167        }
168        Declaration::AxonStore(n) => {
169            n.leading_trivia = leading;
170            n.trailing_trivia = trailing;
171        }
172        Declaration::AxonEndpoint(n) => {
173            n.leading_trivia = leading;
174            n.trailing_trivia = trailing;
175        }
176        Declaration::Resource(n) => {
177            n.leading_trivia = leading;
178            n.trailing_trivia = trailing;
179        }
180        Declaration::Fabric(n) => {
181            n.leading_trivia = leading;
182            n.trailing_trivia = trailing;
183        }
184        Declaration::Manifest(n) => {
185            n.leading_trivia = leading;
186            n.trailing_trivia = trailing;
187        }
188        Declaration::Observe(n) => {
189            n.leading_trivia = leading;
190            n.trailing_trivia = trailing;
191        }
192        Declaration::Reconcile(n) => {
193            n.leading_trivia = leading;
194            n.trailing_trivia = trailing;
195        }
196        Declaration::Lease(n) => {
197            n.leading_trivia = leading;
198            n.trailing_trivia = trailing;
199        }
200        Declaration::Ensemble(n) => {
201            n.leading_trivia = leading;
202            n.trailing_trivia = trailing;
203        }
204        Declaration::Session(n) => {
205            n.leading_trivia = leading;
206            n.trailing_trivia = trailing;
207        }
208        Declaration::Topology(n) => {
209            n.leading_trivia = leading;
210            n.trailing_trivia = trailing;
211        }
212        Declaration::Immune(n) => {
213            n.leading_trivia = leading;
214            n.trailing_trivia = trailing;
215        }
216        Declaration::Reflex(n) => {
217            n.leading_trivia = leading;
218            n.trailing_trivia = trailing;
219        }
220        Declaration::Heal(n) => {
221            n.leading_trivia = leading;
222            n.trailing_trivia = trailing;
223        }
224        Declaration::Component(n) => {
225            n.leading_trivia = leading;
226            n.trailing_trivia = trailing;
227        }
228        Declaration::View(n) => {
229            n.leading_trivia = leading;
230            n.trailing_trivia = trailing;
231        }
232        Declaration::Channel(n) => {
233            n.leading_trivia = leading;
234            n.trailing_trivia = trailing;
235        }
236        Declaration::Socket(n) => {
237            n.leading_trivia = leading;
238            n.trailing_trivia = trailing;
239        }
240        Declaration::Upstream(n) => {
241            n.leading_trivia = leading;
242            n.trailing_trivia = trailing;
243        }
244        Declaration::Voice(n) => {
245            n.leading_trivia = leading;
246            n.trailing_trivia = trailing;
247        }
248        Declaration::Cors(n) => {
249            n.leading_trivia = leading;
250            n.trailing_trivia = trailing;
251        }
252        Declaration::Credential(n) => {
253            n.leading_trivia = leading;
254            n.trailing_trivia = trailing;
255        }
256        Declaration::Cache(n) => {
257            n.leading_trivia = leading;
258            n.trailing_trivia = trailing;
259        }
260        Declaration::Savant(n) => {
261            n.leading_trivia = leading;
262            n.trailing_trivia = trailing;
263        }
264        Declaration::Synth(n) => {
265            n.leading_trivia = leading;
266            n.trailing_trivia = trailing;
267        }
268        Declaration::Scope(n) => {
269            n.leading_trivia = leading;
270            n.trailing_trivia = trailing;
271        }
272        Declaration::Observable(n) => {
273            n.leading_trivia = leading;
274            n.trailing_trivia = trailing;
275        }
276        Declaration::Witness(n) => {
277            n.leading_trivia = leading;
278            n.trailing_trivia = trailing;
279        }
280        Declaration::Document(n) => {
281            n.leading_trivia = leading;
282            n.trailing_trivia = trailing;
283        }
284        Declaration::Deliver(n) => {
285            n.leading_trivia = leading;
286            n.trailing_trivia = trailing;
287        }
288        Declaration::Notify(n) => {
289            n.leading_trivia = leading;
290            n.trailing_trivia = trailing;
291        }
292        Declaration::Generic(n) => {
293            n.leading_trivia = leading;
294            n.trailing_trivia = trailing;
295        }
296    }
297}
298
299// ── Public error type ────────────────────────────────────────────────────────
300
301/// v1.20.0 — Source-context constants. D4 ratified 2026-05-10:
302/// 2 lines before + 2 lines after the error line. Mirror of the
303/// Python-side `_SOURCE_CONTEXT_LINES_BEFORE` / `_AFTER` so the
304/// rustc-style block has identical shape across stacks.
305pub const SOURCE_CONTEXT_LINES_BEFORE: usize = 2;
306pub const SOURCE_CONTEXT_LINES_AFTER: usize = 2;
307
308/// v1.20.0 — Rustc-style source-context block for a parse error.
309///
310/// Holds a reference to the source text plus the line/column the
311/// error points at. Rendering is lazy — call ``render()`` to format
312/// the block (line numbers + caret + 2 lines before + 2 after).
313///
314/// Pure and deterministic: no ANSI colors, no terminal-width
315/// detection. Output shape is byte-identical to the Python
316/// `SourceSnippet.render()` on the same input — that's the cross-
317/// stack drift gate (28.i).
318#[derive(Debug, Clone)]
319pub struct SourceSnippet {
320    pub source: String,
321    pub line: u32,
322    pub column: u32,
323    pub filename: String,
324    pub context_before: usize,
325    pub context_after: usize,
326}
327
328impl SourceSnippet {
329    /// Construct with the default 2/2 context window.
330    pub fn new(source: String, line: u32, column: u32, filename: String) -> Self {
331        Self {
332            source,
333            line,
334            column,
335            filename,
336            context_before: SOURCE_CONTEXT_LINES_BEFORE,
337            context_after: SOURCE_CONTEXT_LINES_AFTER,
338        }
339    }
340
341    /// Format the snippet as a multi-line rustc-style block.
342    ///
343    /// Empty source → empty string. Out-of-range line → empty
344    /// string. Caret column is clamped to `[1, line_len + 1]`.
345    /// Output shape matches Python `SourceSnippet.render` byte-
346    /// identically per D7.
347    #[must_use]
348    pub fn render(&self) -> String {
349        if self.source.is_empty() || self.line < 1 {
350            return String::new();
351        }
352        let raw: Vec<&str> = self.source.split('\n').collect();
353        // Match Python's str.splitlines() trailing-newline shape:
354        // strip an empty trailing entry produced by a final '\n'.
355        let lines: Vec<&str> = if raw.last() == Some(&"") {
356            raw[..raw.len() - 1].to_vec()
357        } else {
358            raw
359        };
360        if lines.is_empty() || self.line as usize > lines.len() {
361            return String::new();
362        }
363
364        let line_idx = self.line as usize;
365        let start = line_idx.saturating_sub(self.context_before).max(1);
366        let end = (line_idx + self.context_after).min(lines.len());
367
368        let gutter = end.to_string().len();
369        let empty_gutter = " ".repeat(gutter);
370
371        let mut out: Vec<String> = Vec::with_capacity(end - start + 4);
372        out.push(format!(
373            "{empty_gutter} --> {}:{}:{}",
374            self.filename, self.line, self.column
375        ));
376        out.push(format!("{empty_gutter} |"));
377        for n in start..=end {
378            let line_text = lines[n - 1];
379            out.push(format!("{n:>gutter$} | {line_text}", gutter = gutter));
380            if n == line_idx {
381                let line_len = line_text.chars().count();
382                let col = (self.column as usize).clamp(1, line_len + 1);
383                out.push(format!(
384                    "{empty_gutter} | {pad}^",
385                    pad = " ".repeat(col - 1)
386                ));
387            }
388        }
389        out.join("\n")
390    }
391}
392
393#[derive(Debug, Clone, Default)]
394pub struct ParseError {
395    pub message: String,
396    pub line: u32,
397    pub column: u32,
398    /// v1.20.0 — Optional rustc-style source-context block.
399    /// `None` preserves the legacy single-line shape; populated by
400    /// `Parser::with_source` callers (and by `parse_with_recovery`
401    /// / `parse` when a source has been attached to the parser).
402    /// Existing struct-literal call sites use the `..Default::default()`
403    /// idiom (default = None) to stay terse.
404    pub source_snippet: Option<SourceSnippet>,
405}
406
407impl ParseError {
408    /// v1.20.0 — Attach a `SourceSnippet` derived from raw source
409    /// text and filename. Returns `self` so the call can be chained
410    /// at the construction site. No-op when `line == 0`. Idempotent.
411    #[must_use]
412    pub fn attach_source(mut self, source: &str, filename: &str) -> Self {
413        if self.line >= 1 {
414            self.source_snippet = Some(SourceSnippet::new(
415                source.to_string(),
416                self.line,
417                self.column,
418                filename.to_string(),
419            ));
420        }
421        self
422    }
423}
424
425impl std::fmt::Display for ParseError {
426    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
427        write!(f, "[line {}:{}] {}", self.line, self.column, self.message)?;
428        if let Some(snippet) = &self.source_snippet {
429            let block = snippet.render();
430            if !block.is_empty() {
431                write!(f, "\n{block}")?;
432            }
433        }
434        Ok(())
435    }
436}
437
438impl std::error::Error for ParseError {}
439
440// ── v1.20.0 — Public recovery result ──────────────────────────────────────
441//
442// Mirror of Python's `axon.compiler.parser.ParseResult` (v1.20.0).
443// The rationale, sync semantics, and test contract are documented in
444// `the design plan`. The Rust frontend
445// must produce structurally identical error lists to the Python parser
446// when handed the same source — that is the cross-stack drift gate
447// (D7 ratified 2026-05-10: byte-identical error lists).
448//
449// `program` holds whatever declarations the parser was able to parse
450// successfully. `errors` holds every recovered error in source order.
451// A clean parse returns `errors.is_empty()`; the existing fail-fast
452// `parse()` API is preserved verbatim per D9.
453
454/// Outcome of `Parser::parse_with_recovery` — partial program plus the
455/// list of every error the parser recovered from. See module docs for
456/// the panic-mode + sync-point recovery semantics.
457#[derive(Debug)]
458pub struct ParseResult {
459    pub program: Program,
460    pub errors: Vec<ParseError>,
461}
462
463impl ParseResult {
464    /// True iff at least one parse error was recovered. Callers that
465    /// want to short-circuit on failure should check this rather than
466    /// relying on `program.declarations.is_empty()` (the parser may
467    /// have salvaged some declarations even with errors present).
468    #[inline]
469    #[must_use]
470    pub fn has_errors(&self) -> bool {
471        !self.errors.is_empty()
472    }
473
474    /// Inverse of `has_errors`. Convenience for the "happy path" check
475    /// in tests + adopter integrations.
476    #[inline]
477    #[must_use]
478    pub fn is_clean(&self) -> bool {
479        self.errors.is_empty()
480    }
481}
482
483/// v1.20.0 — Top-level declaration keywords used as resync points
484/// during error recovery (D2 ratified 2026-05-10). Mirrors the
485/// `_TOP_LEVEL_DECLARATION_KEYWORDS` frozenset on the Python side.
486///
487/// Distinct from `tokens::is_declaration_keyword` because that helper
488/// is used by the structural declaration counter and intentionally
489/// excludes some grammar-only tokens (Know/Believe/Speculate/Doubt,
490/// Ingest, Ots) that DO begin a top-level declaration in
491/// `parse_declaration` and therefore must be valid sync points.
492///
493/// Adding a new top-level dispatch arm in `parse_declaration` MUST
494/// add the corresponding token here so the recovery walker can
495/// re-sync correctly.
496#[inline]
497const fn is_top_level_decl_kw_for_recovery(tt: &TokenType) -> bool {
498    matches!(
499        tt,
500        TokenType::Import
501            | TokenType::Persona
502            | TokenType::Context
503            | TokenType::Anchor
504            | TokenType::Memory
505            | TokenType::Tool
506            | TokenType::Type
507            | TokenType::Flow
508            | TokenType::Intent
509            | TokenType::Run
510            | TokenType::Let
511            | TokenType::Know
512            | TokenType::Believe
513            | TokenType::Speculate
514            | TokenType::Doubt
515            | TokenType::Lambda
516            | TokenType::Agent
517            | TokenType::Shield
518            | TokenType::Pix
519            | TokenType::Ledger
520            | TokenType::Psyche
521            | TokenType::Corpus
522            | TokenType::Dataspace
523            | TokenType::Ots
524            | TokenType::Mandate
525            | TokenType::Compute
526            | TokenType::Daemon
527            // v2.42.0 — the autonomous research primitive + synth policy.
528            | TokenType::Savant
529            | TokenType::Synth
530            // v2.43.0 — the authorization-scope policy declaration.
531            | TokenType::Scope
532            | TokenType::AxonStore
533            | TokenType::AxonEndpoint
534            | TokenType::Resource
535            | TokenType::Fabric
536            | TokenType::Manifest
537            | TokenType::Observe
538            | TokenType::Reconcile
539            | TokenType::Lease
540            | TokenType::Ensemble
541            | TokenType::Session
542            | TokenType::Topology
543            | TokenType::Immune
544            | TokenType::Reflex
545            | TokenType::Heal
546            | TokenType::Component
547            | TokenType::View
548            | TokenType::Channel
549            | TokenType::Ingest
550            | TokenType::Persist
551            | TokenType::Retrieve
552            | TokenType::Mutate
553            | TokenType::Purge
554            | TokenType::Transact
555            | TokenType::Mcp
556    )
557}
558
559// ── v1.21.0 — axonendpoint transport + keepalive closed enums ────────────
560//
561// D2 ratified 2026-05-10: `transport` is a closed enum
562// {json, sse, ndjson}. D6 ratified: `keepalive` is a closed enum
563// {5s, 15s, 30s, 60s}. Both mirror the Python frontend's
564// `_AXONENDPOINT_TRANSPORT_VALUES` / `_AXONENDPOINT_KEEPALIVE_VALUES`
565// frozensets in `axon/compiler/parser.py`. Cross-stack drift gate
566// (30.b fixture) asserts byte-identical parse for every entry.
567
568/// Adopter-facing acceptable values for `transport:` field.
569/// Used by both the parser (validation + smart-suggest) and the
570/// type-checker (30.c) so adopter tooling sees one canonical list.
571pub const AXONENDPOINT_TRANSPORT_VALUES: &[&str] = &["json", "sse", "ndjson"];
572
573/// v1.28.0 — Closed-catalog SSE wire-format
574/// dialects. Selected via the parametrized grammar
575/// `transport: sse(<dialect>)`; bare `transport: sse` resolves to
576/// the Q1 default per the flow's algebraic-effect predicate
577/// (openai for tool-streaming flows; axon for type-annotation-only).
578///
579/// Vertical-grounded scope (Q3 revised 2026-05-14): five dialects
580/// cover ~99% of LLM-streaming adopter expectations.
581///   - `axon`      — current W3C named events
582///                   (event: axon.token / event: axon.complete).
583///                   D6 backwards-compat baseline; indefinitely
584///                   supported as a first-class option.
585///   - `openai`    — `data: {"choices":[{"delta":{...}}]}` frames
586///                   terminated by `data: [DONE]`. OpenAI Chat
587///                   Completions streaming wire verbatim.
588///   - `kimi`      — Moonshot Kimi (kimi.moonshot.cn) — uses the
589///                   OpenAI-compatible Chat Completions wire format
590///                   verbatim (same chunk shape, same `data: [DONE]`
591///                   sentinel). First-class entry so adopters
592///                   declare intent explicitly; under the hood the
593///                   wire is identical to `openai`.
594///   - `glm`       — Zhipu ChatGLM (open.bigmodel.cn) — same as
595///                   kimi, uses OpenAI-compat wire. First-class
596///                   entry for adopter clarity.
597///   - `anthropic` — `event: content_block_delta` frames terminated
598///                   by `event: message_stop`. Adopter SDKs
599///                   targeting Anthropic Claude consume this shape
600///                   verbatim.
601///
602/// Why kimi + glm as first-class entries (Q3 revision rationale):
603/// The project's primary adopter pipelines through Kimi K2.x +
604/// Zhipu GLM-4.x. While the wire IS byte-identical to OpenAI's
605/// Chat Completions streaming, declaring `transport: sse(kimi)` /
606/// `transport: sse(glm)` lets the audit trail + observability
607/// surfaces correlate adopter intent against the underlying
608/// provider — without the adopter having to know that "kimi
609/// happens to be OpenAI-compat on the wire today". The runtime
610/// dispatches kimi + glm to the same `OpenAIDialectAdapter` so
611/// the wire shape stays canonical-OpenAI-bytes.
612///
613/// Open-set adapter pluggability (downstream crates registering
614/// custom dialects) remains explicitly out of scope per the
615/// Axon-for-Axon discipline.
616pub const AXONENDPOINT_TRANSPORT_DIALECTS: &[&str] =
617    &["axon", "openai", "kimi", "glm", "anthropic"];
618
619/// Adopter-facing acceptable values for `keepalive:` field.
620pub const AXONENDPOINT_KEEPALIVE_VALUES: &[&str] = &["5s", "15s", "30s", "60s"];
621
622/// v1.23.0 D3 — Closed method enum for `method:` field. Adopter-
623/// declarable methods only; HEAD/OPTIONS/CONNECT/TRACE are
624/// runtime-managed (CORS preflight, etc.) and never declared from
625/// source. Closed enum refuses interpretation drift; smart-suggest
626/// catches near-misses at parse time.
627///
628/// v2.62.0 — `QUERY` (RFC 10008, Proposed Standard, June 2026): the safe +
629/// idempotent + cacheable method that CARRIES A REQUEST BODY — the first new HTTP
630/// method in two decades. It carries a LAW, not just a route: `axon-T927` refuses
631/// at compile time a QUERY endpoint whose flow performs a declared write (the
632/// RFC's normative "safe and idempotent" MUST, made a proof).
633///
634/// Must stay in lockstep with `type_checker::VALID_ENDPOINT_METHODS`.
635pub const AXONENDPOINT_METHOD_VALUES: &[&str] =
636    &["GET", "POST", "PUT", "DELETE", "PATCH", "QUERY"];
637
638/// v1.31.0 (D2) — Closed catalog for the `axonendpoint backend:`
639/// declaration. The set is `CANONICAL_PROVIDERS ∪ {auto, stub}`:
640///
641///   - the seven canonical LLM providers — `anthropic`, `gemini`,
642///     `glm`, `kimi`, `ollama`, `openai`, `openrouter` — a concrete,
643/// declared backend that rung 2 of the v1.31.0 D1 resolution
644///     ladder fires immediately;
645///   - `auto` — transparent: declaring it is equivalent to omitting
646///     `backend:` entirely (the route resolves down the ladder —
647///     server default → environment-available providers);
648///   - `stub` — the no-op backend, reachable ONLY by an explicit,
649///     written declaration (D5: a silent degradation to `stub` is
650///     forbidden; an explicit opt-in is not).
651///
652/// `axon-frontend` carries zero runtime deps and therefore cannot
653/// import `axon::backends::CANONICAL_PROVIDERS`; this list is a
654/// hand-maintained mirror. The axon-rs drift gate
655/// (`tests/backend_catalog_drift.rs`) asserts the two stay
656/// byte-identical — adding a provider in one place without the other
657/// fails CI.
658pub const AXONENDPOINT_BACKEND_VALUES: &[&str] = &[
659    "anthropic",
660    "auto",
661    "gemini",
662    "glm",
663    "kimi",
664    "ollama",
665    "openai",
666    "openrouter",
667    "stub",
668];
669
670#[inline]
671fn axonendpoint_is_valid_transport(s: &str) -> bool {
672    AXONENDPOINT_TRANSPORT_VALUES.iter().any(|&v| v == s)
673}
674
675#[inline]
676fn axonendpoint_is_valid_method(s: &str) -> bool {
677    AXONENDPOINT_METHOD_VALUES.iter().any(|&v| v == s)
678}
679
680#[inline]
681fn axonendpoint_is_valid_backend(s: &str) -> bool {
682    AXONENDPOINT_BACKEND_VALUES.iter().any(|&v| v == s)
683}
684
685#[inline]
686fn axonendpoint_is_valid_keepalive(s: &str) -> bool {
687    AXONENDPOINT_KEEPALIVE_VALUES.iter().any(|&v| v == s)
688}
689
690/// v1.32.0 (D2) — Closed type catalog for query parameters.
691///
692/// Query values arrive over HTTP as URL-encoded strings; the catalog
693/// is the set of types axon will validate / coerce them into for the
694/// Request Binding Contract. Hand-curated, intentionally small:
695///   - `Text` — the raw string (always succeeds)
696///   - `Int` — `i64` parseable
697///   - `Float` — `f64` parseable, finite
698///   - `Bool` — case-insensitive `{true, false, 1, 0, yes, no, on, off}`
699///   - `Uuid` — RFC 4122 textual form
700///
701/// Extending the catalog is a future axon-T?nn surface; v1.38.5 ships
702/// the 5 types covering ~95% of REST query patterns. Lists / dates /
703/// datetimes / enums are honest deferrals (see section 7 of the plan vivo).
704pub const AXONENDPOINT_QUERY_PARAM_TYPES: &[&str] =
705    &["Text", "Int", "Float", "Bool", "Uuid"];
706
707/// `true` iff `s` is one of the v1.32.0 (D2) query-param catalog
708/// entries — exact case-sensitive match (axon types are PascalCase).
709#[inline]
710pub(crate) fn axonendpoint_is_valid_query_param_type(s: &str) -> bool {
711    AXONENDPOINT_QUERY_PARAM_TYPES.iter().any(|&v| v == s)
712}
713
714/// v1.32.0 (D1) — Extract `{name}` placeholder names from an
715/// `axonendpoint` `path:` string, in left-to-right declaration order.
716///
717/// Recognized placeholder grammar (single-segment, no nested braces):
718/// `{NAME}` where `NAME` matches `[A-Za-z_][A-Za-z0-9_]*`. Anything
719/// inside braces that does NOT match the identifier shape is silently
720/// IGNORED — it's either an adopter typo (caught later by axum at
721/// route registration) or a literal brace in the URL pattern.
722///
723/// Returns `Err(duplicate_name)` when the same `{name}` appears more
724/// than once in the path — HTTP route patterns reject duplicates
725/// structurally (`axum` would panic at registration), so surfacing
726/// the error at parse time is the right place.
727///
728/// Pure + total: never panics; deterministic over its single string
729/// argument. Hand-rolled scanner (no regex dep at parser layer).
730///
731/// # Examples
732///
733/// - `"/api/users"` → `Ok(vec![])`
734/// - `"/api/users/{id}"` → `Ok(vec!["id"])`
735/// - `"/api/tenants/{tenant_id}/secrets/{secret_name}"`
736///   → `Ok(vec!["tenant_id", "secret_name"])`
737/// - `"/api/users/{id}/posts/{id}"` → `Err("id")` (duplicate)
738/// - `"/api/{not valid}"` → `Ok(vec![])` (malformed brace content
739///   silently ignored; axum surfaces the error at registration)
740pub(crate) fn extract_path_param_names(path: &str) -> Result<Vec<String>, String> {
741    let mut out: Vec<String> = Vec::new();
742    let bytes = path.as_bytes();
743    let mut i = 0;
744    while i < bytes.len() {
745        if bytes[i] != b'{' {
746            i += 1;
747            continue;
748        }
749        // Find the matching close brace; if none, the open brace is
750        // a literal — leave it alone.
751        let start = i + 1;
752        let mut end = start;
753        while end < bytes.len() && bytes[end] != b'}' {
754            end += 1;
755        }
756        if end == bytes.len() {
757            // Unterminated — give up; downstream parser/runtime
758            // surface the malformed path elsewhere.
759            break;
760        }
761        let raw = &path[start..end];
762        // Validate identifier shape: [A-Za-z_][A-Za-z0-9_]*
763        let valid = !raw.is_empty()
764            && raw.bytes().enumerate().all(|(idx, b)| {
765                if idx == 0 {
766                    b.is_ascii_alphabetic() || b == b'_'
767                } else {
768                    b.is_ascii_alphanumeric() || b == b'_'
769                }
770            });
771        if valid {
772            let name = raw.to_string();
773            if out.iter().any(|existing| existing == &name) {
774                return Err(name);
775            }
776            out.push(name);
777        }
778        i = end + 1;
779    }
780    Ok(out)
781}
782
783/// v1.23.0 (D8) — Closed capability-slug grammar. Validates a
784/// `requires:` slug per `^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$`.
785///
786/// Hand-rolled (no regex dep at parser layer) — each segment must
787/// match `[a-z][a-z0-9_]*` and segments are joined by single dots.
788/// Public so the runtime mirror (`axon::auth_scope`) reuses the same
789/// predicate without duplicating the rule.
790///
791/// Examples valid: `admin`, `legal.read`, `hipaa.phi.read`,
792/// `bank.officer.senior`, `a`, `a_b`, `a1`.
793/// Examples invalid: empty, `Admin` (uppercase), `1admin` (digit
794/// first), `bank-officer` (hyphen), `bank..a` (empty segment),
795/// `.admin`, `admin.`, `admin..` .
796pub fn is_valid_capability_slug(slug: &str) -> bool {
797    if slug.is_empty() {
798        return false;
799    }
800    for segment in slug.split('.') {
801        if !is_valid_slug_segment(segment) {
802            return false;
803        }
804    }
805    true
806}
807
808fn is_valid_slug_segment(seg: &str) -> bool {
809    let mut chars = seg.chars();
810    let first = match chars.next() {
811        Some(c) => c,
812        None => return false,
813    };
814    if !first.is_ascii_lowercase() {
815        return false;
816    }
817    chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
818}
819
820// ════════════════════════════════════════════════════════════════════
821// v1.32.0 (D1) — `extract_path_param_names` unit tests
822// ════════════════════════════════════════════════════════════════════
823
824// ════════════════════════════════════════════════════════════════════
825// v1.32.0 (D2) — `axonendpoint_is_valid_query_param_type` + the
826//  inline `query: { … }` parser, end-to-end through the lexer.
827// ════════════════════════════════════════════════════════════════════
828
829#[cfg(test)]
830mod query_param_catalog_tests {
831    use super::{axonendpoint_is_valid_query_param_type, AXONENDPOINT_QUERY_PARAM_TYPES};
832
833    #[test]
834    fn accepts_every_catalog_entry() {
835        for ty in AXONENDPOINT_QUERY_PARAM_TYPES {
836            assert!(
837                axonendpoint_is_valid_query_param_type(ty),
838                "catalog entry `{ty}` must validate"
839            );
840        }
841    }
842
843    #[test]
844    fn rejects_off_catalog_types() {
845        for off in &[
846            "Timestamp",    // not in v1.38.5 — list/dates deferred
847            "Date",
848            "DateTime",
849            "List<Text>", // multi-value query params deferred (section 7)
850            "Jsonb",        // store-only types not query-applicable
851            "Bytea",
852            "text",         // lowercase rejected (axon types are PascalCase)
853            "TEXT",
854            "Number",       // not in axon's type catalog at all
855            "",             // empty
856            " ",            // whitespace
857        ] {
858            assert!(
859                !axonendpoint_is_valid_query_param_type(off),
860                "off-catalog `{off}` must reject"
861            );
862        }
863    }
864
865    #[test]
866    fn catalog_size_matches_design() {
867        // The plan vivo D2 states a closed 5-type catalog. A future
868        // axon-T?nn surface may extend it; that requires updating BOTH
869        // the catalog AND the plan vivo section 7 honest-scope note.
870        assert_eq!(AXONENDPOINT_QUERY_PARAM_TYPES.len(), 5);
871    }
872}
873
874#[cfg(test)]
875mod query_param_parser_tests {
876    use crate::lexer::Lexer;
877    use crate::parser::Parser;
878
879    fn parse_endpoint_source(src: &str) -> Result<crate::ast::AxonEndpointDefinition, String> {
880        let tokens = Lexer::new(src, "test.axon")
881            .tokenize()
882            .map_err(|e| format!("lex: {}", e.message))?;
883        let mut parser = Parser::new(tokens);
884        let program = parser.parse().map_err(|e| format!("parse: {}", e.message))?;
885        program
886            .declarations
887            .into_iter()
888            .find_map(|d| match d {
889                crate::ast::Declaration::AxonEndpoint(e) => Some(e),
890                _ => None,
891            })
892            .ok_or_else(|| "no axonendpoint in program".to_string())
893    }
894
895    #[test]
896    fn endpoint_with_no_query_block_keeps_empty_vec() {
897        let src = r#"
898            axonendpoint write_secret {
899                method: POST
900                path: "/api/users"
901                body: SecretWriteRequest
902                execute: WriteSecret
903            }
904        "#;
905        let ep = parse_endpoint_source(src).expect("parses");
906        assert!(
907            ep.query_params.is_empty(),
908            "D5 — no `query:` block ⇒ empty query_params"
909        );
910    }
911
912    #[test]
913    fn single_query_param_required() {
914        let src = r#"
915            axonendpoint list_users {
916                method: GET
917                path: "/api/users"
918                query: { status: Text }
919                execute: ListUsers
920            }
921        "#;
922        let ep = parse_endpoint_source(src).expect("parses");
923        assert_eq!(ep.query_params.len(), 1);
924        assert_eq!(ep.query_params[0].name, "status");
925        assert_eq!(ep.query_params[0].type_expr.name, "Text");
926        assert!(!ep.query_params[0].type_expr.optional);
927    }
928
929    #[test]
930    fn optional_query_param_via_question_suffix() {
931        let src = r#"
932            axonendpoint list_users {
933                method: GET
934                path: "/api/users"
935                query: { limit: Int? }
936                execute: ListUsers
937            }
938        "#;
939        let ep = parse_endpoint_source(src).expect("parses");
940        assert_eq!(ep.query_params.len(), 1);
941        assert_eq!(ep.query_params[0].name, "limit");
942        assert_eq!(ep.query_params[0].type_expr.name, "Int");
943        assert!(
944            ep.query_params[0].type_expr.optional,
945            "`?` suffix sets optional"
946        );
947    }
948
949    #[test]
950    fn multiple_query_params_preserve_declaration_order() {
951        let src = r#"
952            axonendpoint search {
953                method: GET
954                path: "/api/search"
955                query: { q: Text, page: Int?, limit: Int?, exact: Bool? }
956                execute: Search
957            }
958        "#;
959        let ep = parse_endpoint_source(src).expect("parses");
960        let names: Vec<&str> = ep.query_params.iter().map(|f| f.name.as_str()).collect();
961        assert_eq!(names, vec!["q", "page", "limit", "exact"]);
962        let types: Vec<&str> = ep
963            .query_params
964            .iter()
965            .map(|f| f.type_expr.name.as_str())
966            .collect();
967        assert_eq!(types, vec!["Text", "Int", "Int", "Bool"]);
968        let optionals: Vec<bool> = ep
969            .query_params
970            .iter()
971            .map(|f| f.type_expr.optional)
972            .collect();
973        assert_eq!(optionals, vec![false, true, true, true]);
974    }
975
976    #[test]
977    fn duplicate_query_param_is_parse_error() {
978        let src = r#"
979            axonendpoint bad {
980                method: GET
981                path: "/api/x"
982                query: { name: Text, name: Int? }
983                execute: Bad
984            }
985        "#;
986        let err = parse_endpoint_source(src).expect_err("must fail");
987        assert!(
988            err.contains("duplicate query param 'name'"),
989            "error must name the duplicate. Got: {err}"
990        );
991    }
992
993    #[test]
994    fn off_catalog_type_with_smart_suggest_hint() {
995        // `Strng` is one edit away from `Text` (would suggest `Text`?
996        // Actually edit distance to `Text` is 4; to `Int` is 5. Likely
997        // no smart suggestion within distance 2. The error still names
998        // the catalog explicitly.)
999        let src = r#"
1000            axonendpoint bad {
1001                method: GET
1002                path: "/api/x"
1003                query: { value: Strng }
1004                execute: Bad
1005            }
1006        "#;
1007        let err = parse_endpoint_source(src).expect_err("must fail");
1008        assert!(
1009            err.contains("unsupported type 'Strng'"),
1010            "error must name the bad type. Got: {err}"
1011        );
1012        assert!(
1013            err.contains("Expected one of: Text | Int | Float | Bool | Uuid"),
1014            "error must list the closed catalog. Got: {err}"
1015        );
1016    }
1017
1018    #[test]
1019    fn close_typo_gets_did_you_mean_hint() {
1020        // `Txt` → edit distance 1 from `Text` → smart-suggest should
1021        // surface the hint.
1022        let src = r#"
1023            axonendpoint bad {
1024                method: GET
1025                path: "/api/x"
1026                query: { value: Txt }
1027                execute: Bad
1028            }
1029        "#;
1030        let err = parse_endpoint_source(src).expect_err("must fail");
1031        assert!(
1032            err.contains("Did you mean") && err.contains("`Text`"),
1033            "smart-suggest must hint `Text`. Got: {err}"
1034        );
1035    }
1036
1037    #[test]
1038    fn every_catalog_type_parses_cleanly() {
1039        // Round-trip smoke for all 5 catalog entries.
1040        for ty in &["Text", "Int", "Float", "Bool", "Uuid"] {
1041            let src = format!(
1042                r#"
1043                    axonendpoint x {{
1044                        method: GET
1045                        path: "/api/x"
1046                        query: {{ v: {ty} }}
1047                        execute: X
1048                    }}
1049                "#
1050            );
1051            let ep = parse_endpoint_source(&src)
1052                .unwrap_or_else(|e| panic!("`{ty}` should parse: {e}"));
1053            assert_eq!(ep.query_params[0].type_expr.name, *ty);
1054        }
1055    }
1056
1057    #[test]
1058    fn comma_optional_between_params() {
1059        // The plan vivo design accepts both comma-separated and
1060        // whitespace-separated query params (existing parser style is
1061        // forgiving). Whitespace-only:
1062        let src = r#"
1063            axonendpoint x {
1064                method: GET
1065                path: "/api/x"
1066                query: { a: Text b: Int? }
1067                execute: X
1068            }
1069        "#;
1070        let ep = parse_endpoint_source(src).expect("parses without commas");
1071        assert_eq!(ep.query_params.len(), 2);
1072    }
1073
1074    // ─── Robustness hardening (37.y.2 100% robust closure) ──────────
1075
1076    #[test]
1077    fn double_query_block_is_parse_error() {
1078        // An adopter who copy-pastes the `query:` block twice should
1079        // see a clear parse error, not a silent merge that produces
1080        // an unexpectedly-augmented endpoint with both blocks fused.
1081        let src = r#"
1082            axonendpoint x {
1083                method: GET
1084                path: "/api/x"
1085                query: { a: Text }
1086                query: { b: Int? }
1087                execute: X
1088            }
1089        "#;
1090        let err = parse_endpoint_source(src).expect_err("must fail");
1091        assert!(
1092            err.contains("declares `query: { … }` more than once"),
1093            "error must call out the duplicate block. Got: {err}"
1094        );
1095        assert!(
1096            err.contains("combine all params into a single block"),
1097            "error must hint the canonical fix. Got: {err}"
1098        );
1099    }
1100
1101    #[test]
1102    fn optional_generic_type_is_parse_error_with_canonical_hint() {
1103        // `Optional<Text>` is the wrong way to declare an optional
1104        // query param. The canonical syntax is `Text?` (the `?`
1105        // suffix). The error must surface this with a literal example.
1106        let src = r#"
1107            axonendpoint x {
1108                method: GET
1109                path: "/api/x"
1110                query: { value: Optional<Text> }
1111                execute: X
1112            }
1113        "#;
1114        let err = parse_endpoint_source(src).expect_err("must fail");
1115        assert!(
1116            err.contains("generic type `Optional<Text>`"),
1117            "error must name the generic type literally. Got: {err}"
1118        );
1119        assert!(
1120            err.contains("Use `Text?` (the `?` suffix)"),
1121            "error must hint the canonical `Text?` syntax. Got: {err}"
1122        );
1123    }
1124
1125    #[test]
1126    fn list_generic_type_is_parse_error_with_deferral_hint() {
1127        // Multi-value query params (`?tag=a&tag=b`) are honest-
1128        // deferred per the plan vivo section 7. Adopters who write
1129        // `List<Text>` should see a clear error explaining the
1130        // deferral, not a confusing "type `List` not in catalog".
1131        let src = r#"
1132            axonendpoint x {
1133                method: GET
1134                path: "/api/x"
1135                query: { tags: List<Text> }
1136                execute: X
1137            }
1138        "#;
1139        let err = parse_endpoint_source(src).expect_err("must fail");
1140        assert!(
1141            err.contains("generic type `List<Text>`"),
1142            "error must name the generic type. Got: {err}"
1143        );
1144        assert!(
1145            err.contains("Multi-value query params")
1146                && err.contains("honest-deferred"),
1147            "error must mention the multi-value deferral. Got: {err}"
1148        );
1149    }
1150
1151    #[test]
1152    fn other_generic_types_caught_generically() {
1153        // Generic types beyond `Optional` and `List` get the
1154        // generic-rejection message without a canonical-syntax hint
1155        // (the catalog list is the canonical guidance).
1156        let src = r#"
1157            axonendpoint x {
1158                method: GET
1159                path: "/api/x"
1160                query: { value: Stream<Int> }
1161                execute: X
1162            }
1163        "#;
1164        let err = parse_endpoint_source(src).expect_err("must fail");
1165        assert!(
1166            err.contains("generic type `Stream<Int>`"),
1167            "error must name the generic type. Got: {err}"
1168        );
1169        assert!(
1170            err.contains("Text | Int | Float | Bool | Uuid"),
1171            "error must list the closed catalog. Got: {err}"
1172        );
1173    }
1174
1175    #[test]
1176    fn uuid_optional_parses_cleanly() {
1177        // Hardening companion — `Uuid?` is in the catalog AND
1178        // optional. The two features compose without surprise.
1179        let src = r#"
1180            axonendpoint find {
1181                method: GET
1182                path: "/api/x"
1183                query: { after: Uuid? }
1184                execute: Find
1185            }
1186        "#;
1187        let ep = parse_endpoint_source(src).expect("parses");
1188        assert_eq!(ep.query_params.len(), 1);
1189        assert_eq!(ep.query_params[0].name, "after");
1190        assert_eq!(ep.query_params[0].type_expr.name, "Uuid");
1191        assert!(ep.query_params[0].type_expr.optional);
1192        assert_eq!(ep.query_params[0].type_expr.generic_param, "");
1193    }
1194
1195    #[test]
1196    fn empty_query_block_yields_empty_vec() {
1197        // `query: { }` is grammatically valid but semantically a
1198        // no-op (equivalent to omitting the block). Don't error;
1199        // just record an empty Vec.
1200        let src = r#"
1201            axonendpoint x {
1202                method: GET
1203                path: "/api/x"
1204                query: { }
1205                execute: X
1206            }
1207        "#;
1208        let ep = parse_endpoint_source(src).expect("empty block parses");
1209        assert!(ep.query_params.is_empty());
1210    }
1211
1212    #[test]
1213    fn kivi_secret_write_path_plus_query() {
1214        // Combined path-param + query-param test: an endpoint that
1215        // takes IDs in the URL AND optional filters in the query
1216        // string. This is the natural REST shape v1.32.0 serves.
1217        let src = r#"
1218            axonendpoint write_secret {
1219                method: POST
1220                path: "/api/tenants/{tenant_id}/secrets/{secret_name}"
1221                query: { dry_run: Bool?, overwrite: Bool? }
1222                body: SecretWriteRequest
1223                execute: WriteSecret
1224            }
1225        "#;
1226        let ep = parse_endpoint_source(src).expect("parses");
1227        // Path params populated (from 37.y.1):
1228        assert_eq!(ep.path_params, vec!["tenant_id", "secret_name"]);
1229        // Query params populated (from this step 37.y.2):
1230        assert_eq!(ep.query_params.len(), 2);
1231        assert_eq!(ep.query_params[0].name, "dry_run");
1232        assert_eq!(ep.query_params[0].type_expr.name, "Bool");
1233        assert!(ep.query_params[0].type_expr.optional);
1234        assert_eq!(ep.query_params[1].name, "overwrite");
1235        // Body still works:
1236        assert_eq!(ep.body_type, "SecretWriteRequest");
1237    }
1238}
1239
1240#[cfg(test)]
1241mod path_param_extraction_tests {
1242    use super::extract_path_param_names;
1243
1244    #[test]
1245    fn empty_path_no_placeholders() {
1246        assert_eq!(extract_path_param_names("/api/users"), Ok(vec![]));
1247        assert_eq!(extract_path_param_names("/"), Ok(vec![]));
1248        assert_eq!(extract_path_param_names(""), Ok(vec![]));
1249    }
1250
1251    #[test]
1252    fn single_placeholder() {
1253        assert_eq!(
1254            extract_path_param_names("/api/users/{id}"),
1255            Ok(vec!["id".to_string()])
1256        );
1257    }
1258
1259    #[test]
1260    fn multiple_placeholders_in_declaration_order() {
1261        assert_eq!(
1262            extract_path_param_names(
1263                "/api/tenants/{tenant_id}/secrets/{secret_name}"
1264            ),
1265            Ok(vec![
1266                "tenant_id".to_string(),
1267                "secret_name".to_string(),
1268            ])
1269        );
1270    }
1271
1272    #[test]
1273    fn kivi_chat_history_path_pattern() {
1274        // The exact pattern the kivi adopter reported (2026-05-20):
1275        // POST /api/tenants/{tenant_id}/secrets/{secret_name}
1276        // Both names extracted in source order.
1277        let names = extract_path_param_names(
1278            "/api/tenants/{tenant_id}/secrets/{secret_name}",
1279        );
1280        assert_eq!(
1281            names,
1282            Ok(vec![
1283                "tenant_id".to_string(),
1284                "secret_name".to_string(),
1285            ])
1286        );
1287    }
1288
1289    #[test]
1290    fn duplicate_placeholder_returns_err() {
1291        assert_eq!(
1292            extract_path_param_names("/api/users/{id}/posts/{id}"),
1293            Err("id".to_string())
1294        );
1295    }
1296
1297    #[test]
1298    fn underscore_and_numeric_in_name() {
1299        assert_eq!(
1300            extract_path_param_names("/api/{user_id}/items/{item_2}"),
1301            Ok(vec!["user_id".to_string(), "item_2".to_string()])
1302        );
1303    }
1304
1305    #[test]
1306    fn leading_underscore_accepted() {
1307        // Identifiers in HTTP paths often start with letters but the
1308        // grammar permits leading underscore (parity with Rust identifier
1309        // rules). The flow parameter name on the binding side has to
1310        // match exactly, so adopters with `_internal_id` in the path
1311        // can pair it with a same-named flow param.
1312        assert_eq!(
1313            extract_path_param_names("/api/{_internal}"),
1314            Ok(vec!["_internal".to_string()])
1315        );
1316    }
1317
1318    #[test]
1319    fn malformed_placeholder_silently_ignored() {
1320        // Content inside `{...}` that does not match the identifier
1321        // grammar is skipped at this layer. axum surfaces the route
1322        // registration failure if the literal text is invalid.
1323        assert_eq!(
1324            extract_path_param_names("/api/{not valid}"),
1325            Ok(vec![])
1326        );
1327        // Empty braces — same: skip silently.
1328        assert_eq!(extract_path_param_names("/api/{}"), Ok(vec![]));
1329        // Mixed: malformed brace skipped, valid placeholder kept.
1330        assert_eq!(
1331            extract_path_param_names("/api/{tenant id}/users/{id}"),
1332            Ok(vec!["id".to_string()])
1333        );
1334    }
1335
1336    #[test]
1337    fn unterminated_brace_returns_clean() {
1338        // Open brace with no close brace — give up without panicking.
1339        // (axum surfaces the malformed-route error at registration.)
1340        assert_eq!(extract_path_param_names("/api/{id"), Ok(vec![]));
1341    }
1342
1343    #[test]
1344    fn placeholders_at_path_boundaries() {
1345        // Placeholder as the very first segment AND the very last
1346        // segment — both should be extracted.
1347        assert_eq!(
1348            extract_path_param_names("{prefix}/api/users/{id}"),
1349            Ok(vec!["prefix".to_string(), "id".to_string()])
1350        );
1351        assert_eq!(
1352            extract_path_param_names("/api/{id}"),
1353            Ok(vec!["id".to_string()])
1354        );
1355    }
1356
1357    #[test]
1358    fn deduplication_detects_non_adjacent_duplicates() {
1359        // The duplicate-detection sweep is global, not just adjacent.
1360        assert_eq!(
1361            extract_path_param_names(
1362                "/api/orgs/{org_id}/teams/{team_id}/repos/{org_id}"
1363            ),
1364            Err("org_id".to_string())
1365        );
1366    }
1367
1368    #[test]
1369    fn never_panics_on_arbitrary_input() {
1370        // Light fuzz: a handful of weird inputs return cleanly.
1371        for input in &[
1372            "{",
1373            "}",
1374            "{}",
1375            "{{}}",
1376            "{{{",
1377            "/api/{}/{id}",
1378            "////",
1379            "\u{1F4A1}",        // emoji (lightbulb)
1380            "\u{0000}",         // null byte
1381        ] {
1382            let _ = extract_path_param_names(input); // must not panic
1383        }
1384    }
1385}
1386
1387#[cfg(test)]
1388mod capability_slug_tests {
1389    use super::is_valid_capability_slug;
1390
1391    #[test]
1392    fn accepts_canonical_examples() {
1393        assert!(is_valid_capability_slug("admin"));
1394        assert!(is_valid_capability_slug("legal.read"));
1395        assert!(is_valid_capability_slug("hipaa.phi.read"));
1396        assert!(is_valid_capability_slug("bank.officer.senior"));
1397        assert!(is_valid_capability_slug("a"));
1398        assert!(is_valid_capability_slug("a_b"));
1399        assert!(is_valid_capability_slug("a1"));
1400        assert!(is_valid_capability_slug("a.b1_c"));
1401    }
1402
1403    #[test]
1404    fn rejects_empty_string() {
1405        assert!(!is_valid_capability_slug(""));
1406    }
1407
1408    #[test]
1409    fn rejects_uppercase() {
1410        assert!(!is_valid_capability_slug("Admin"));
1411        assert!(!is_valid_capability_slug("admin.READ"));
1412    }
1413
1414    #[test]
1415    fn rejects_digit_first() {
1416        assert!(!is_valid_capability_slug("1admin"));
1417        assert!(!is_valid_capability_slug("admin.1read"));
1418    }
1419
1420    #[test]
1421    fn rejects_hyphen() {
1422        assert!(!is_valid_capability_slug("bank-officer"));
1423    }
1424
1425    #[test]
1426    fn rejects_empty_segments() {
1427        assert!(!is_valid_capability_slug("bank..a"));
1428        assert!(!is_valid_capability_slug(".admin"));
1429        assert!(!is_valid_capability_slug("admin."));
1430    }
1431
1432    #[test]
1433    fn rejects_special_chars() {
1434        assert!(!is_valid_capability_slug("admin@read"));
1435        assert!(!is_valid_capability_slug("admin/read"));
1436        assert!(!is_valid_capability_slug("admin read"));
1437    }
1438}
1439
1440// ── Parser ───────────────────────────────────────────────────────────────────
1441
1442pub struct Parser {
1443    tokens: Vec<Token>,
1444    pos: usize,
1445    /// v2.83.0 — declarations lifted out of a FLOW BODY to program level.
1446    ///
1447    /// README nests an epistemic block inside a flow to scope the helper
1448    /// flows it calls:
1449    ///
1450    /// ```text
1451    /// flow MarketIntelligence(sector: String) -> Report {
1452    ///     know { flow GatherData(sector: String) -> DataSet { … } }
1453    ///     par { … }
1454    /// }
1455    /// ```
1456    ///
1457    /// A top-level `know { … }` already HOISTS its children into the
1458    /// program-level IR collections, stamping `epistemic_mode` on each
1459    /// (`ir_generator`, v2.53.0/v2.60.0/v2.66.0). Hoisting the nested one to a
1460    /// top-level `Declaration::Epistemic` therefore makes it byte-identical
1461    /// to the form that already works — zero new handling in the checker, the
1462    /// IR generator, or the runtime. The alternative (a new FlowStep variant
1463    /// carrying declarations) would fork every one of those.
1464    hoisted: Vec<Declaration>,
1465    /// v1.5.2 — leading trivia parallel array, indexed by the
1466    /// effective-token position. `leading_trivia[i]` is the comment
1467    /// trivia that appeared between the previous effective token (or
1468    /// file start) and `tokens[i]`.
1469    leading_trivia: Vec<Vec<Trivia>>,
1470    /// v1.5.2 — trailing trivia parallel array. `trailing_trivia[i]`
1471    /// is the comment trivia on the same line as `tokens[i]`, before
1472    /// the next effective token. Populated by the constructor.
1473    trailing_trivia: Vec<Vec<Trivia>>,
1474    /// v1.12.0 — side-channel for tagging let value_kind. Set by
1475    /// `parse_let_atom` / `parse_let_value_expr` as they descend; read
1476    /// at the end of `parse_let` and stored on the LetStatement.
1477    last_let_value_kind: String,
1478    /// v1.14.0 — loop nesting depth for break/continue scope check.
1479    /// Incremented at the start of `parse_for_in`, decremented after.
1480    /// `parse_break`/`parse_continue` raise ParseError when this is
1481    /// zero (the keyword has no meaning outside a loop body).
1482    loop_depth: u32,
1483    /// v1.20.0 — Optional source text + filename for the rustc-
1484    /// style source-context block on `ParseError`. Set via the
1485    /// fluent `Parser::with_source` builder; default `None` keeps
1486    /// existing callers (`Parser::new(tokens).parse()`) emitting
1487    /// the legacy single-line shape.
1488    source: Option<String>,
1489    filename: String,
1490}
1491
1492impl Parser {
1493    pub fn new(raw_tokens: Vec<Token>) -> Self {
1494        // ── v1.5.2 — split the raw token stream into:
1495        //   - effective tokens the grammar consumes (cursor advances
1496        //     over these as before),
1497        //   - parallel `leading_trivia` / `trailing_trivia` arrays
1498        //     indexed by effective-token position.
1499        // Comments on a fresh line attach as leading trivia of the
1500        // next effective token; comments on the same line as an
1501        // effective token attach as trailing trivia of that token.
1502        // Roslyn/Swift convention.
1503        let mut effective: Vec<Token> = Vec::with_capacity(raw_tokens.len());
1504        let mut leading: Vec<Vec<Trivia>> = Vec::with_capacity(raw_tokens.len());
1505        let mut trailing: Vec<Vec<Trivia>> = Vec::with_capacity(raw_tokens.len());
1506
1507        let mut pending_leading: Vec<Trivia> = Vec::new();
1508        let mut last_effective_line: i64 = -1;
1509        for tok in raw_tokens {
1510            if is_comment_token(&tok.ttype) {
1511                let kind = token_to_trivia_kind(&tok.ttype)
1512                    .expect("comment token must map to a trivia kind");
1513                let triv = Trivia {
1514                    kind,
1515                    text: tok.value,
1516                    line: tok.line,
1517                    column: tok.column,
1518                };
1519                if !effective.is_empty() && (tok.line as i64) == last_effective_line {
1520                    trailing.last_mut().unwrap().push(triv);
1521                } else {
1522                    pending_leading.push(triv);
1523                }
1524            } else {
1525                last_effective_line = tok.line as i64;
1526                effective.push(tok);
1527                leading.push(std::mem::take(&mut pending_leading));
1528                trailing.push(Vec::new());
1529            }
1530        }
1531
1532        Parser {
1533            hoisted: Vec::new(),
1534            tokens: effective,
1535            pos: 0,
1536            leading_trivia: leading,
1537            trailing_trivia: trailing,
1538            last_let_value_kind: "literal".to_string(),
1539            loop_depth: 0,
1540            source: None,
1541            filename: "<source>".to_string(),
1542        }
1543    }
1544
1545    /// v1.20.0 — Fluent attach of source text + filename for
1546    /// rustc-style source-context blocks on emitted `ParseError`s.
1547    /// Returns `self` so it chains with `.parse_with_recovery()`:
1548    ///
1549    /// ```ignore
1550    /// let result = Parser::new(tokens)
1551    ///     .with_source(src, "foo.axon")
1552    ///     .parse_with_recovery();
1553    /// ```
1554    ///
1555    /// No-op of any other behaviour — pure metadata attach.
1556    #[must_use]
1557    pub fn with_source(mut self, source: &str, filename: &str) -> Self {
1558        self.source = Some(source.to_string());
1559        self.filename = filename.to_string();
1560        self
1561    }
1562
1563    // ── public API ───────────────────────────────────────────────
1564
1565    pub fn parse(&mut self) -> Result<Program, ParseError> {
1566        let mut program = Program {
1567            declarations: Vec::new(),
1568            declaration_trivia: Vec::new(),
1569            loc: Loc { line: 1, column: 1 },
1570        };
1571        while !self.check(TokenType::Eof) {
1572            // Capture trivia around the declaration. `start_pos` is
1573            // the effective-token position of the declaration's first
1574            // token; that position carries the leading trivia. After
1575            // parsing, `pos - 1` is the last token consumed; that
1576            // position carries the trailing trivia.
1577            let start_pos = self.pos;
1578            let mut decl = match self.parse_declaration() {
1579                Ok(d) => d,
1580                Err(e) => return Err(self.attach_source_to_error(e)),
1581            };
1582            let end_pos = self.pos.saturating_sub(1);
1583            let leading = self
1584                .leading_trivia
1585                .get(start_pos)
1586                .cloned()
1587                .unwrap_or_default();
1588            let trailing = self
1589                .trailing_trivia
1590                .get(end_pos)
1591                .cloned()
1592                .unwrap_or_default();
1593            // v1.5.2 — also copy trivia into the per-struct fields on
1594            // the declaration so consumers can read `flow.leading_trivia`
1595            // directly without going through `program.declaration_trivia[i]`.
1596            // The side-channel is preserved for backward compat with
1597            // 14.a callers and as a flat enumeration source.
1598            attach_trivia_to_decl(&mut decl, leading.clone(), trailing.clone());
1599            program.declarations.push(decl);
1600            program
1601                .declaration_trivia
1602                .push(DeclarationTrivia { leading, trailing });
1603            // v2.83.0 — drain anything a flow body hoisted to program
1604            // level. Appended AFTER the enclosing declaration so source order
1605            // still reads top-to-bottom in `axon desugar`.
1606            for hoisted in std::mem::take(&mut self.hoisted) {
1607                program.declarations.push(hoisted);
1608                program.declaration_trivia.push(DeclarationTrivia {
1609                    leading: Vec::new(),
1610                    trailing: Vec::new(),
1611                });
1612            }
1613        }
1614        // v2.37.0 — expand `voice` declarations FIRST (they may emit
1615        // `from Preset@vN` upstream legs), then v2.37.0 preset references,
1616        // BEFORE type-check — so the v2.37.0 laws and the IR see the expanded
1617        // program (and `axon desugar` prints exactly this lowering).
1618        // Unknown presets stay unexpanded — the checker reports them with
1619        // the catalog list (accumulating diagnostics beat a parse abort).
1620        crate::voice_desugar::expand(&mut program);
1621        crate::upstream_presets::expand(&mut program);
1622        Ok(program)
1623    }
1624
1625    // ── v1.20.0 — recovery-mode parse ─────────────────────────
1626    //
1627    // Mirror of Python's `Parser.parse_with_recovery` from
1628    // `axon/compiler/parser.py`. Wraps `parse_declaration` in a
1629    // try/recover loop: on any `ParseError` the error is appended to
1630    // the list and the cursor advances to the next sync point, then
1631    // parsing resumes. The two stacks must produce structurally
1632    // identical error lists on the same input — that is the cross-
1633    // stack drift gate (D7). See the test module
1634    // `tests::recovery_tests` and Python-side
1635    // `tests/test_fase28_parser_recovery.py`.
1636
1637    /// Recovery-mode parse. Collects every parse error in source
1638    /// order; the existing `parse()` API remains fail-fast (D9).
1639    ///
1640    /// # Recovery contract (D2)
1641    ///
1642    /// On `ParseError`:
1643    ///   1. Push the error onto `errors`.
1644    ///   2. If the cursor is already on a top-level declaration
1645    ///      keyword (and brace-depth ≤ 0), do not consume — the
1646    ///      caller should retry the declaration parse from here.
1647    ///      Otherwise advance one token to make progress, then
1648    ///      walk to the next sync point.
1649    ///   3. Resume the outer loop.
1650    ///
1651    /// Sync points: top-level declaration keyword at brace-depth ≤ 0,
1652    /// or EOF. Negative depths are treated identically to ≤ 0 — the
1653    /// walker keeps walking through over-balanced `}` rather than
1654    /// pretending a closing brace is itself a sync point (which would
1655    /// emit a ghost "Unexpected token at top level" error in the
1656    /// outer loop).
1657    pub fn parse_with_recovery(&mut self) -> ParseResult {
1658        let mut program = Program {
1659            declarations: Vec::new(),
1660            declaration_trivia: Vec::new(),
1661            loc: Loc { line: 1, column: 1 },
1662        };
1663        let mut errors: Vec<ParseError> = Vec::new();
1664
1665        while !self.check(TokenType::Eof) {
1666            let start_pos = self.pos;
1667            match self.parse_declaration() {
1668                Ok(mut decl) => {
1669                    let end_pos = self.pos.saturating_sub(1);
1670                    let leading = self
1671                        .leading_trivia
1672                        .get(start_pos)
1673                        .cloned()
1674                        .unwrap_or_default();
1675                    let trailing = self
1676                        .trailing_trivia
1677                        .get(end_pos)
1678                        .cloned()
1679                        .unwrap_or_default();
1680                    attach_trivia_to_decl(&mut decl, leading.clone(), trailing.clone());
1681                    program.declarations.push(decl);
1682                    program
1683                        .declaration_trivia
1684                        .push(DeclarationTrivia { leading, trailing });
1685                }
1686                Err(err) => {
1687                    // v1.20.0 — attach source-context block when a
1688                    // source has been provided via `with_source(...)`;
1689                    // otherwise the error keeps its single-line shape.
1690                    errors.push(self.attach_source_to_error(err));
1691                    // Make progress. If parse_declaration returned
1692                    // immediately on the same token (e.g. unknown
1693                    // top-level token), we MUST advance at least one
1694                    // token to avoid an infinite loop.
1695                    if self.pos == start_pos && !self.check(TokenType::Eof) {
1696                        self.advance();
1697                    }
1698                    self.advance_to_sync_point();
1699                }
1700            }
1701        }
1702
1703        ParseResult { program, errors }
1704    }
1705
1706    /// v1.20.0 — Decorate a `ParseError` with a `SourceSnippet`
1707    /// when the parser has source context attached, otherwise return
1708    /// the error unchanged. Idempotent: if the error already carries
1709    /// a snippet, this overwrites it with the parser's source.
1710    fn attach_source_to_error(&self, err: ParseError) -> ParseError {
1711        match &self.source {
1712            Some(src) if err.line >= 1 => err.attach_source(src, &self.filename),
1713            _ => err,
1714        }
1715    }
1716
1717    /// v1.20.0 — Walk the cursor forward until the next sync
1718    /// point (top-level declaration keyword at brace-depth ≤ 0) or
1719    /// EOF. Used by `parse_with_recovery` to skip the malformed
1720    /// remainder of a failed declaration.
1721    fn advance_to_sync_point(&mut self) {
1722        let mut depth: i32 = 0;
1723        while !self.check(TokenType::Eof) {
1724            let tt = self.current().ttype.clone();
1725            // Sync at top-level keywords when depth ≤ 0. We do not
1726            // consume the keyword — the outer loop will dispatch on
1727            // it.
1728            if is_top_level_decl_kw_for_recovery(&tt) && depth <= 0 {
1729                return;
1730            }
1731            if matches!(tt, TokenType::LBrace) {
1732                depth += 1;
1733            } else if matches!(tt, TokenType::RBrace) {
1734                depth -= 1;
1735            }
1736            self.advance();
1737        }
1738    }
1739
1740    // ── token helpers ────────────────────────────────────────────
1741
1742    fn current(&self) -> &Token {
1743        if self.pos >= self.tokens.len() {
1744            self.tokens.last().unwrap() // EOF sentinel
1745        } else {
1746            &self.tokens[self.pos]
1747        }
1748    }
1749
1750    fn advance(&mut self) -> &Token {
1751        let idx = self.pos;
1752        if self.pos < self.tokens.len() {
1753            self.pos += 1;
1754        }
1755        &self.tokens[idx]
1756    }
1757
1758    fn check(&self, tt: TokenType) -> bool {
1759        self.current().ttype == tt
1760    }
1761
1762    fn consume(&mut self, expected: TokenType) -> Result<Token, ParseError> {
1763        let tok = self.current().clone();
1764        if tok.ttype != expected {
1765            return Err(ParseError {
1766                message: format!(
1767                    "Expected {:?}, found {:?}('{}')",
1768                    expected, tok.ttype, tok.value
1769                ),
1770                line: tok.line,
1771                column: tok.column,
1772                            ..Default::default()
1773            });
1774        }
1775        self.pos += 1;
1776        Ok(tok)
1777    }
1778
1779    /// v2.3.0 — build a `ParseError` at the current token's location.
1780    fn error(&self, message: &str) -> ParseError {
1781        let tok = self.current();
1782        ParseError { message: message.to_string(), line: tok.line, column: tok.column, ..Default::default() }
1783    }
1784
1785    /// Consume any identifier or keyword-used-as-value.
1786    fn consume_any_ident_or_kw(&mut self) -> Result<Token, ParseError> {
1787        let tok = self.current().clone();
1788        match tok.ttype {
1789            TokenType::Identifier
1790            | TokenType::Bool
1791            | TokenType::StringLit
1792            | TokenType::Integer
1793            | TokenType::Float => {
1794                self.pos += 1;
1795                Ok(tok)
1796            }
1797            _ => {
1798                // Allow any keyword token whose value is alphabetic
1799                if !tok.value.is_empty()
1800                    && tok.value.chars().all(|c| c.is_alphanumeric() || c == '_')
1801                    && tok.ttype != TokenType::Eof
1802                {
1803                    self.pos += 1;
1804                    Ok(tok)
1805                } else {
1806                    Err(ParseError {
1807                        message: format!(
1808                            "Expected identifier or keyword value, found {:?}('{}')",
1809                            tok.ttype, tok.value
1810                        ),
1811                        line: tok.line,
1812                        column: tok.column,
1813                                            ..Default::default()
1814                    })
1815                }
1816            }
1817        }
1818    }
1819
1820    fn consume_number(&mut self) -> Result<f64, ParseError> {
1821        let tok = self.current().clone();
1822        match tok.ttype {
1823            TokenType::Float | TokenType::Integer => {
1824                self.pos += 1;
1825                tok.value.parse::<f64>().map_err(|_| ParseError {
1826                    message: format!("Invalid number '{}'", tok.value),
1827                    line: tok.line,
1828                    column: tok.column,
1829                                    ..Default::default()
1830                })
1831            }
1832            _ => Err(ParseError {
1833                message: format!("Expected number, found {:?}('{}')", tok.ttype, tok.value),
1834                line: tok.line,
1835                column: tok.column,
1836                            ..Default::default()
1837            }),
1838        }
1839    }
1840
1841    fn parse_bool(&mut self) -> Result<bool, ParseError> {
1842        let tok = self.consume(TokenType::Bool)?;
1843        Ok(tok.value == "true")
1844    }
1845
1846    fn loc_of(&self, tok: &Token) -> Loc {
1847        Loc {
1848            line: tok.line,
1849            column: tok.column,
1850        }
1851    }
1852
1853    fn check_run_modifier(&self) -> bool {
1854        // v2.83.0 — `with <Persona>` is the spelling README uses on every
1855        // `run` it publishes; `as <Persona>` is the one the parser took. Same
1856        // position, same meaning, and the two cannot be confused: `with` is not
1857        // a keyword token, and the only OTHER `with` in the language sits after
1858        // a tool name inside a step body (`use_tool T with k: v`), which this
1859        // predicate is never consulted at.
1860        if self.current().value == "with" {
1861            return true;
1862        }
1863        matches!(
1864            self.current().ttype,
1865            TokenType::As
1866                | TokenType::Within
1867                | TokenType::ConstrainedBy
1868                | TokenType::OnFailure
1869                | TokenType::OutputTo
1870                | TokenType::Effort
1871        )
1872    }
1873
1874    // ── list helpers ─────────────────────────────────────────────
1875
1876    fn parse_string_list(&mut self) -> Result<Vec<String>, ParseError> {
1877        self.consume(TokenType::LBracket)?;
1878        let mut items = Vec::new();
1879        items.push(self.consume(TokenType::StringLit)?.value);
1880        while self.check(TokenType::Comma) {
1881            self.advance();
1882            items.push(self.consume(TokenType::StringLit)?.value);
1883        }
1884        self.consume(TokenType::RBracket)?;
1885        Ok(items)
1886    }
1887
1888    /// v2.38.0 — a bracketed list of quoted string literals, tolerant of
1889    /// an empty `[]` and a trailing comma before `]` (the `Window.exclude`
1890    /// shape, generalized into a reusable helper). Used for CORS field
1891    /// lists whose values contain characters (`://`, `.`, `-`) that aren't
1892    /// valid bare identifiers — `allow_origins`, `allow_headers`,
1893    /// `expose_headers` — where `parse_string_list`'s "at least one item,
1894    /// no trailing comma" strictness would reject a legitimate empty or
1895    /// comma-terminated declaration.
1896    fn parse_bracketed_strings(&mut self) -> Result<Vec<String>, ParseError> {
1897        self.consume(TokenType::LBracket)?;
1898        let mut items = Vec::new();
1899        if !self.check(TokenType::RBracket) {
1900            items.push(self.consume(TokenType::StringLit)?.value);
1901            while self.check(TokenType::Comma) {
1902                self.advance();
1903                if self.check(TokenType::RBracket) {
1904                    break; // trailing comma
1905                }
1906                items.push(self.consume(TokenType::StringLit)?.value);
1907            }
1908        }
1909        self.consume(TokenType::RBracket)?;
1910        Ok(items)
1911    }
1912
1913    fn parse_identifier_list(&mut self) -> Result<Vec<String>, ParseError> {
1914        let mut names = Vec::new();
1915        names.push(self.consume(TokenType::Identifier)?.value);
1916        while self.check(TokenType::Comma) {
1917            self.advance();
1918            names.push(self.consume(TokenType::Identifier)?.value);
1919        }
1920        Ok(names)
1921    }
1922
1923    fn parse_bracketed_identifiers(&mut self) -> Result<Vec<String>, ParseError> {
1924        self.consume(TokenType::LBracket)?;
1925        let items = self.parse_extended_identifier_list()?;
1926        self.consume(TokenType::RBracket)?;
1927        Ok(items)
1928    }
1929
1930    fn parse_extended_identifier_list(&mut self) -> Result<Vec<String>, ParseError> {
1931        let mut items = Vec::new();
1932        items.push(self.consume_any_ident_or_kw()?.value);
1933        while self.check(TokenType::Comma) {
1934            self.advance();
1935            items.push(self.consume_any_ident_or_kw()?.value);
1936        }
1937        Ok(items)
1938    }
1939
1940    fn parse_dotted_identifier(&mut self) -> Result<String, ParseError> {
1941        let mut parts = vec![self.consume_any_ident_or_kw()?.value];
1942        while self.check(TokenType::Dot) {
1943            self.advance();
1944            parts.push(self.consume_any_ident_or_kw()?.value);
1945        }
1946        Ok(parts.join("."))
1947    }
1948
1949    /// v2.83.0 — a **SUBJECT**: the thing a statement acts ON.
1950    ///
1951    /// Every statement in the language has two kinds of operand, and they had
1952    /// been parsed by the same function:
1953    ///
1954    ///   - a **NAME** — the declaration being applied (`compute CalculatePremium`,
1955    ///     `mandate SECFormat`, `use_tool WebSearch`). Always a bare identifier;
1956    ///     a dotted name would refer to nothing.
1957    ///   - a **SUBJECT** — what it acts on (`validate Assess.output`,
1958    ///     `compute X on Analyze.risk_factor, 1.2`). A reference, and a
1959    ///     reference in Axon is DOTTED: `Assess.output` is the canonical way one
1960    ///     step names another's result, and it already parses inside `given:`,
1961    ///     inside `use_tool … with k: v`, and in `navigate_ref`.
1962    ///
1963    /// Subject positions called `consume_any_ident_or_kw`, which stops at the
1964    /// dot. So the reference form the whole language is built on was rejected in
1965    /// exactly the position that most needs it — five README blocks fail on it
1966    /// as their FIRST error and three more need it further in.
1967    ///
1968    /// Literals are subjects too (`compute X on Profile.tenure, 1.2, "USD"`).
1969    /// A string literal keeps its quotes here so the runtime can tell a literal
1970    /// from a binding name — the v2.10.0 classification, preserved instead of
1971    /// flattened.
1972    fn parse_subject(&mut self) -> Result<String, ParseError> {
1973        let t = self.current().clone();
1974        match t.ttype {
1975            TokenType::StringLit => {
1976                self.advance();
1977                Ok(format!("\"{}\"", t.value))
1978            }
1979            TokenType::Integer | TokenType::Float => {
1980                self.advance();
1981                Ok(t.value)
1982            }
1983            _ => self.parse_dotted_identifier(),
1984        }
1985    }
1986
1987    fn parse_expression_string(&mut self) -> Result<String, ParseError> {
1988        if self.check(TokenType::LBracket) {
1989            let items = self.parse_bracketed_dot_identifiers()?;
1990            return Ok(format!("[{}]", items.join(", ")));
1991        }
1992        self.parse_dotted_identifier()
1993    }
1994
1995    fn parse_bracketed_dot_identifiers(&mut self) -> Result<Vec<String>, ParseError> {
1996        self.consume(TokenType::LBracket)?;
1997        let mut items = vec![self.parse_dotted_identifier()?];
1998        while self.check(TokenType::Comma) {
1999            self.advance();
2000            items.push(self.parse_dotted_identifier()?);
2001        }
2002        self.consume(TokenType::RBracket)?;
2003        Ok(items)
2004    }
2005
2006    fn parse_argument_list(&mut self) -> Result<Vec<String>, ParseError> {
2007        let mut args = Vec::new();
2008        while !self.check(TokenType::RParen) {
2009            let tok = self.current().clone();
2010            match tok.ttype {
2011                TokenType::StringLit | TokenType::Integer | TokenType::Float => {
2012                    self.advance();
2013                    args.push(tok.value);
2014                }
2015                TokenType::Identifier => {
2016                    self.advance();
2017                    let mut val = tok.value;
2018                    if self.check(TokenType::Dot) {
2019                        self.advance();
2020                        val.push('.');
2021                        val.push_str(&self.consume_any_ident_or_kw()?.value);
2022                    }
2023                    args.push(val);
2024                }
2025                _ => {
2026                    self.advance();
2027                    let key = tok.value;
2028                    if self.check(TokenType::Colon) {
2029                        self.advance();
2030                        let v = self.advance().value.clone();
2031                        args.push(format!("{key}:{v}"));
2032                    } else {
2033                        args.push(key);
2034                    }
2035                }
2036            }
2037            if self.check(TokenType::Comma) {
2038                self.advance();
2039            }
2040        }
2041        Ok(args)
2042    }
2043
2044    /// Skip a single value or balanced bracketed/braced block (unknown field).
2045    fn skip_value(&mut self) {
2046        match self.current().ttype {
2047            TokenType::LBracket => {
2048                self.advance();
2049                let mut depth = 1u32;
2050                while depth > 0 && !self.check(TokenType::Eof) {
2051                    if self.check(TokenType::LBracket) {
2052                        depth += 1;
2053                    } else if self.check(TokenType::RBracket) {
2054                        depth -= 1;
2055                    }
2056                    self.advance();
2057                }
2058            }
2059            TokenType::LBrace => {
2060                self.advance();
2061                let mut depth = 1u32;
2062                while depth > 0 && !self.check(TokenType::Eof) {
2063                    if self.check(TokenType::LBrace) {
2064                        depth += 1;
2065                    } else if self.check(TokenType::RBrace) {
2066                        depth -= 1;
2067                    }
2068                    self.advance();
2069                }
2070            }
2071            TokenType::Lt => {
2072                // effect row: <io, network, ...>
2073                self.advance();
2074                let mut depth = 1u32;
2075                while depth > 0 && !self.check(TokenType::Eof) {
2076                    if self.check(TokenType::Lt) {
2077                        depth += 1;
2078                    } else if self.check(TokenType::Gt) {
2079                        depth -= 1;
2080                    }
2081                    self.advance();
2082                }
2083            }
2084            _ => {
2085                self.advance();
2086                while self.check(TokenType::Dot) {
2087                    self.advance();
2088                    self.advance();
2089                }
2090            }
2091        }
2092    }
2093
2094    /// Skip a balanced `{ ... }` block including its braces.
2095    fn skip_braced_block(&mut self) -> Result<(), ParseError> {
2096        self.consume(TokenType::LBrace)?;
2097        let mut depth = 1u32;
2098        while depth > 0 {
2099            if self.check(TokenType::Eof) {
2100                let tok = self.current();
2101                return Err(ParseError {
2102                    message: "Unterminated block — expected '}'".to_string(),
2103                    line: tok.line,
2104                    column: tok.column,
2105                                    ..Default::default()
2106                });
2107            }
2108            if self.check(TokenType::LBrace) {
2109                depth += 1;
2110            } else if self.check(TokenType::RBrace) {
2111                depth -= 1;
2112            }
2113            self.advance();
2114        }
2115        Ok(())
2116    }
2117
2118    fn at_declaration_start(&self) -> bool {
2119        is_declaration_keyword(&self.current().ttype) || self.check(TokenType::Eof)
2120    }
2121
2122    // ── top-level dispatch ───────────────────────────────────────
2123
2124    fn parse_declaration(&mut self) -> Result<Declaration, ParseError> {
2125        let tok = self.current().clone();
2126
2127        // v2.69.0 — a TOP-LEVEL `budget <Name> { … }`.
2128        //
2129        // `budget` lexes as `TokenType::Budget` (the daemon-field keyword). At top
2130        // level it is only a declaration when a NAME follows — `budget Foo { … }`.
2131        // The lookahead is what keeps the daemon-attached form (`daemon D { budget
2132        // { … } }`, where `{` follows immediately) untouched: there the next token
2133        // is `{`, not an identifier, so this branch does not fire.
2134        if tok.ttype == TokenType::Budget && self.peek_is_identifier() {
2135            return self.parse_top_level_budget().map(Declaration::Budget);
2136        }
2137
2138        match tok.ttype {
2139            TokenType::Import => self.parse_import().map(Declaration::Import),
2140            TokenType::Persona => self.parse_persona().map(Declaration::Persona),
2141            TokenType::Context => self.parse_context().map(Declaration::Context),
2142            TokenType::Anchor => self.parse_anchor().map(Declaration::Anchor),
2143            TokenType::Memory => self.parse_memory().map(Declaration::Memory),
2144            TokenType::Tool => self.parse_tool().map(Declaration::Tool),
2145            TokenType::Type => self.parse_type_def().map(Declaration::Type),
2146            TokenType::Flow => self.parse_flow().map(Declaration::Flow),
2147            // v2.87.0 — `effect E { Op(p: T) -> R }`. A peer of `tool`, per
2148            // `the design plan` section 3.1 ("top-level, like tool/persona/anchor").
2149            TokenType::Effect => self.parse_effect().map(Declaration::Effect),
2150            TokenType::Intent => self.parse_intent().map(Declaration::Intent),
2151            TokenType::Run => self.parse_run().map(Declaration::Run),
2152            TokenType::Let => self.parse_let().map(Declaration::Let),
2153            TokenType::Know | TokenType::Believe | TokenType::Speculate | TokenType::Doubt => {
2154                self.parse_epistemic_block().map(Declaration::Epistemic)
2155            }
2156            TokenType::Lambda => self.parse_lambda_data().map(Declaration::LambdaData),
2157
2158            // ── Tier 2 declarations (full AST) ──────────────────
2159            TokenType::Agent => self.parse_agent().map(Declaration::Agent),
2160            TokenType::Shield => self.parse_shield().map(Declaration::Shield),
2161            // v2.27.0 — temporal execution-window guard.
2162            TokenType::Window => self.parse_window().map(Declaration::Window),
2163            TokenType::Pix => self.parse_pix().map(Declaration::Pix),
2164            TokenType::Ledger => self.parse_ledger().map(Declaration::Ledger),
2165            TokenType::Psyche => self.parse_psyche().map(Declaration::Psyche),
2166            TokenType::Corpus => self.parse_corpus().map(Declaration::Corpus),
2167            TokenType::Dataspace => self.parse_dataspace().map(Declaration::Dataspace),
2168            TokenType::Ots => self.parse_ots().map(Declaration::Ots),
2169            TokenType::Mandate => self.parse_mandate().map(Declaration::Mandate),
2170            TokenType::Compute => self.parse_compute().map(Declaration::Compute),
2171            TokenType::Daemon => self.parse_daemon().map(Declaration::Daemon),
2172            TokenType::Extension => self.parse_extension().map(Declaration::Extension),
2173            TokenType::AxonStore => self.parse_axonstore().map(Declaration::AxonStore),
2174            TokenType::AxonEndpoint => self.parse_axonendpoint().map(Declaration::AxonEndpoint),
2175
2176            // ── v1.1.0 — I/O cognitivo ───────────────────
2177            TokenType::Resource => self.parse_resource().map(Declaration::Resource),
2178            TokenType::Fabric => self.parse_fabric().map(Declaration::Fabric),
2179            TokenType::Manifest => self.parse_manifest().map(Declaration::Manifest),
2180            TokenType::Observe => self.parse_observe().map(Declaration::Observe),
2181
2182            // ── v1.1.0 — Control cognitivo ───────────────
2183            TokenType::Reconcile => self.parse_reconcile().map(Declaration::Reconcile),
2184            TokenType::Lease => self.parse_lease().map(Declaration::Lease),
2185            TokenType::Ensemble => self.parse_ensemble().map(Declaration::Ensemble),
2186
2187            // ── v1.1.0 — Topology + π-calculus sessions ─
2188            TokenType::Session => self.parse_session_definition().map(Declaration::Session),
2189            TokenType::Topology => self.parse_topology().map(Declaration::Topology),
2190
2191            // ── v2.3.0 — typed WebSocket transport ─────────
2192            TokenType::Socket => self.parse_socket().map(Declaration::Socket),
2193
2194            // ── v2.37.0 — outbound vendor connection ─────────
2195            TokenType::Upstream => self.parse_upstream().map(Declaration::Upstream),
2196
2197            // ── v2.37.0 — the voice-agent simplicity layer ───
2198            TokenType::Voice => self.parse_voice().map(Declaration::Voice),
2199
2200            // ── v2.38.0 — the named origin-policy declaration ─
2201            TokenType::Cors => self.parse_cors().map(Declaration::Cors),
2202
2203            // ── v2.40.0 — the named result-memoization policy ─
2204            TokenType::Cache => self.parse_cache().map(Declaration::Cache),
2205            TokenType::Document => self.parse_document().map(Declaration::Document),
2206
2207            // ── v2.60.0 — Governed CRM Delivery ─
2208            TokenType::Deliver => self.parse_deliver().map(Declaration::Deliver),
2209            TokenType::Notify => self.parse_notify().map(Declaration::Notify),
2210
2211            // ── v2.42.0 — the long-horizon autonomous research primitive ─
2212            TokenType::Savant => self.parse_savant().map(Declaration::Savant),
2213
2214            // ── v2.42.0 — the dynamic tool-synthesis policy ──────────────
2215            TokenType::Synth => self.parse_synth().map(Declaration::Synth),
2216
2217            // ── v2.43.0 — the authorization-scope policy declaration ─────
2218            TokenType::Scope => self.parse_scope().map(Declaration::Scope),
2219
2220            // ── v2.46.0 — the ephemeral-credential contract ──────────────
2221            TokenType::Credential => self.parse_credential().map(Declaration::Credential),
2222
2223            // ── v2.4.0 — Pauli-sum observable ────────────
2224            TokenType::Observable => self.parse_observable().map(Declaration::Observable),
2225
2226            // ── v2.23.0 — Advantage Witness ──────────────────
2227            TokenType::Witness => self.parse_witness().map(Declaration::Witness),
2228
2229            // ── v1.1.0 — Cognitive immune system ─────────
2230            TokenType::Immune => self.parse_immune().map(Declaration::Immune),
2231            TokenType::Reflex => self.parse_reflex().map(Declaration::Reflex),
2232            TokenType::Heal => self.parse_heal().map(Declaration::Heal),
2233
2234            // ── v1.3.1 — UI cognitiva ────────────────────
2235            TokenType::Component => self.parse_component().map(Declaration::Component),
2236            TokenType::View => self.parse_view().map(Declaration::View),
2237
2238            // ── v1.6.0 — Mobile typed channels ──────────
2239            TokenType::Channel => self.parse_channel().map(Declaration::Channel),
2240
2241            // ── Tier 3+ structural fallback ─────────────────────
2242            // Store operations: keyword target { ... } or keyword target ...
2243            TokenType::Ingest
2244            | TokenType::Persist
2245            | TokenType::Retrieve
2246            | TokenType::Mutate
2247            | TokenType::Purge
2248            | TokenType::Transact => self.parse_generic_declaration(),
2249
2250            // MCP declaration
2251            TokenType::Mcp => self.parse_generic_declaration(),
2252
2253            _ => {
2254                // v1.20.0 — append "Did you mean X?" hint when the
2255                // unknown token looks like a typo'd top-level keyword
2256                // (Levenshtein ≤ 2). D3, D11 ratified 2026-05-10.
2257                let hint = crate::smart_suggest::suggest_for(
2258                    &tok.value,
2259                    crate::smart_suggest::TOP_LEVEL_KEYWORD_NAMES,
2260                );
2261                let base = format!(
2262                    "Unexpected token at top level: '{}' — expected declaration \
2263                     (persona, context, anchor, flow, run, ...)",
2264                    tok.value
2265                );
2266                let message = if hint.is_empty() {
2267                    base
2268                } else {
2269                    format!("{base}. {hint}")
2270                };
2271                Err(ParseError {
2272                    message,
2273                    line: tok.line,
2274                    column: tok.column,
2275                    ..Default::default()
2276                })
2277            }
2278        }
2279    }
2280
2281    // ── IMPORT ───────────────────────────────────────────────────
2282
2283    fn parse_import(&mut self) -> Result<ImportNode, ParseError> {
2284        let tok = self.consume(TokenType::Import)?;
2285        let loc = self.loc_of(&tok);
2286
2287        let mut path_parts = Vec::new();
2288
2289        // Optional @ scope
2290        if self.check(TokenType::At) {
2291            self.advance();
2292            let first = self.consume(TokenType::Identifier)?;
2293            path_parts.push(format!("@{}", first.value));
2294        } else {
2295            let first = self.consume(TokenType::Identifier)?;
2296            path_parts.push(first.value);
2297        }
2298
2299        while self.check(TokenType::Dot) {
2300            self.advance();
2301            if self.check(TokenType::LBrace) {
2302                break;
2303            }
2304            let part = self.consume(TokenType::Identifier)?;
2305            path_parts.push(part.value);
2306        }
2307
2308        let mut names = Vec::new();
2309        if self.check(TokenType::LBrace) {
2310            self.advance();
2311            names = self.parse_identifier_list()?;
2312            self.consume(TokenType::RBrace)?;
2313        }
2314
2315        // ── v2.76.0 — the `@allow_downgrade` ECC valve ───────────────
2316        //
2317        // `import a.b.{X} @allow_downgrade` acknowledges an epistemic
2318        // downgrade across this edge (see `epistemic_compat.rs`). The
2319        // annotation position is unambiguous: no top-level declaration
2320        // begins with `@`, so an `@` here belongs to this import — and an
2321        // unknown annotation is refused with the fix in the message
2322        // rather than surfacing later as an opaque parse error.
2323        let mut allow_downgrade = false;
2324        if self.check(TokenType::At) {
2325            let at_tok = self.current().clone();
2326            self.advance();
2327            let ident = self.consume(TokenType::Identifier)?;
2328            if ident.value == "allow_downgrade" {
2329                allow_downgrade = true;
2330            } else {
2331                return Err(ParseError {
2332                    message: format!(
2333                        "unknown import annotation '@{}' — the only import annotation is \
2334                         `@allow_downgrade` (the epistemic-downgrade acknowledgment).",
2335                        ident.value
2336                    ),
2337                    line: at_tok.line,
2338                    column: at_tok.column,
2339                    ..Default::default()
2340                });
2341            }
2342        }
2343
2344        // ── v2.67.0 — `apx` is RETRACTED ───────────────────────────────
2345        //
2346        // `import X with apx { … }` used to parse and then call
2347        // `skip_braced_block()` — the policy was consumed and thrown on the
2348        // floor. It never reached the AST, let alone the IR. In `axon-rs` the
2349        // string "apx" occurred only inside comments: there is no APX crate,
2350        // no binary, no MEC/PCC dependency verification, no EPR ranking, no
2351        // quarantine and no compliance gate. The public README advertised all
2352        // five.
2353        //
2354        // A dependency policy that silently evaporates is the worst possible
2355        // shape for this particular promise: the adopter believes their supply
2356        // chain is being verified, which is exactly the belief that stops them
2357        // from verifying it themselves. Refuse, loudly.
2358        let next_is_apx = self
2359            .tokens
2360            .get(self.pos + 1)
2361            .map(|t| t.value == "apx")
2362            .unwrap_or(false);
2363        if self.current().value == "with" && next_is_apx {
2364            let tok = self.current().clone();
2365            return Err(ParseError {
2366                message: "`import … with apx { … }` is RETRACTED (v2.67.0). The apx policy block was \
2367                          parsed and silently DISCARDED — it never reached the IR, and no epistemic \
2368                          dependency manager exists: no MEC/PCC verification, no EPR ranking, no \
2369                          quarantine, no compliance gate. Declaring it verified nothing while \
2370                          implying your supply chain was checked. Remove the `with apx { … }` \
2371                          clause; the plain `import` resolves through the Epistemic Module \
2372                          System."
2373                    .to_string(),
2374                line: tok.line,
2375                column: tok.column,
2376                ..Default::default()
2377            });
2378        }
2379
2380        Ok(ImportNode {
2381            module_path: path_parts,
2382            names,
2383            allow_downgrade,
2384            loc,
2385            leading_trivia: Vec::new(),
2386            trailing_trivia: Vec::new(),
2387        })
2388    }
2389
2390    // ── PERSONA ──────────────────────────────────────────────────
2391
2392    fn parse_persona(&mut self) -> Result<PersonaDefinition, ParseError> {
2393        let tok = self.consume(TokenType::Persona)?;
2394        let loc = self.loc_of(&tok);
2395        let name = self.consume(TokenType::Identifier)?.value;
2396        self.consume(TokenType::LBrace)?;
2397
2398        let mut node = PersonaDefinition {
2399            name,
2400            domain: Vec::new(),
2401            tone: String::new(),
2402            confidence_threshold: None,
2403            cite_sources: None,
2404            refuse_if: Vec::new(),
2405            language: String::new(),
2406            description: String::new(),
2407            loc,
2408            leading_trivia: Vec::new(),
2409            trailing_trivia: Vec::new(),
2410        };
2411
2412        while !self.check(TokenType::RBrace) {
2413            let field_name = self.current().value.clone();
2414            self.advance();
2415            self.consume(TokenType::Colon)?;
2416
2417            match field_name.as_str() {
2418                "domain" => node.domain = self.parse_string_list()?,
2419                "tone" => node.tone = self.consume_any_ident_or_kw()?.value,
2420                "confidence_threshold" => node.confidence_threshold = Some(self.consume_number()?),
2421                "cite_sources" => node.cite_sources = Some(self.parse_bool()?),
2422                "refuse_if" => node.refuse_if = self.parse_bracketed_identifiers()?,
2423                "language" => node.language = self.consume(TokenType::StringLit)?.value,
2424                "description" => node.description = self.consume(TokenType::StringLit)?.value,
2425                _ => self.skip_value(),
2426            }
2427        }
2428        self.consume(TokenType::RBrace)?;
2429        Ok(node)
2430    }
2431
2432    // ── CONTEXT ──────────────────────────────────────────────────
2433
2434    fn parse_context(&mut self) -> Result<ContextDefinition, ParseError> {
2435        let tok = self.consume(TokenType::Context)?;
2436        let loc = self.loc_of(&tok);
2437        let name = self.consume(TokenType::Identifier)?.value;
2438        self.consume(TokenType::LBrace)?;
2439
2440        let mut node = ContextDefinition {
2441            name,
2442            memory_scope: String::new(),
2443            language: String::new(),
2444            depth: String::new(),
2445            max_tokens: None,
2446            temperature: None,
2447            cite_sources: None,
2448            now_tz: None,
2449            loc,
2450            leading_trivia: Vec::new(),
2451            trailing_trivia: Vec::new(),
2452        };
2453
2454        while !self.check(TokenType::RBrace) {
2455            let field_name = self.current().value.clone();
2456            self.advance();
2457            self.consume(TokenType::Colon)?;
2458
2459            match field_name.as_str() {
2460                "memory" => node.memory_scope = self.consume_any_ident_or_kw()?.value,
2461                "language" => node.language = self.consume(TokenType::StringLit)?.value,
2462                "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
2463                // v2.46.0 — the frame's cognitive timezone (IANA string).
2464                "now" => node.now_tz = Some(self.consume(TokenType::StringLit)?.value),
2465                "max_tokens" => {
2466                    node.max_tokens = Some(
2467                        self.consume(TokenType::Integer)?
2468                            .value
2469                            .parse::<i64>()
2470                            .unwrap_or(0),
2471                    )
2472                }
2473                "temperature" => node.temperature = Some(self.consume_number()?),
2474                "cite_sources" => node.cite_sources = Some(self.parse_bool()?),
2475                _ => self.skip_value(),
2476            }
2477        }
2478        self.consume(TokenType::RBrace)?;
2479        Ok(node)
2480    }
2481
2482    // ── ANCHOR ───────────────────────────────────────────────────
2483
2484    fn parse_anchor(&mut self) -> Result<AnchorConstraint, ParseError> {
2485        let tok = self.consume(TokenType::Anchor)?;
2486        let loc = self.loc_of(&tok);
2487        let name = self.consume(TokenType::Identifier)?.value;
2488        self.consume(TokenType::LBrace)?;
2489
2490        let mut node = AnchorConstraint {
2491            name,
2492            require: String::new(),
2493            reject: Vec::new(),
2494            enforce: String::new(),
2495            description: String::new(),
2496            confidence_floor: None,
2497            unknown_response: String::new(),
2498            on_violation: String::new(),
2499            on_violation_target: String::new(),
2500            loc,
2501            leading_trivia: Vec::new(),
2502            trailing_trivia: Vec::new(),
2503        };
2504
2505        while !self.check(TokenType::RBrace) {
2506            let field_name = self.current().value.clone();
2507            self.advance();
2508            self.consume(TokenType::Colon)?;
2509
2510            match field_name.as_str() {
2511                "require" => node.require = self.consume_any_ident_or_kw()?.value,
2512                "description" => node.description = self.consume(TokenType::StringLit)?.value,
2513                "reject" => node.reject = self.parse_bracketed_identifiers()?,
2514                "enforce" => node.enforce = self.consume_any_ident_or_kw()?.value,
2515                "confidence_floor" => node.confidence_floor = Some(self.consume_number()?),
2516                "unknown_response" => {
2517                    node.unknown_response = self.consume(TokenType::StringLit)?.value
2518                }
2519                "on_violation" => {
2520                    // Parse: raise ErrorName | fallback(...) | identifier
2521                    let action = self.consume_any_ident_or_kw()?.value;
2522                    node.on_violation = action.clone();
2523                    if action == "raise" || action == "fallback" {
2524                        node.on_violation_target = self.consume_any_ident_or_kw()?.value;
2525                    }
2526                }
2527                _ => self.skip_value(),
2528            }
2529        }
2530        self.consume(TokenType::RBrace)?;
2531        Ok(node)
2532    }
2533
2534    // ── MEMORY ───────────────────────────────────────────────────
2535
2536    fn parse_memory(&mut self) -> Result<MemoryDefinition, ParseError> {
2537        let tok = self.consume(TokenType::Memory)?;
2538        let loc = self.loc_of(&tok);
2539        let name = self.consume(TokenType::Identifier)?.value;
2540        self.consume(TokenType::LBrace)?;
2541
2542        let mut node = MemoryDefinition {
2543            name,
2544            store: String::new(),
2545            backend: String::new(),
2546            retrieval: String::new(),
2547            decay: String::new(),
2548            loc,
2549            leading_trivia: Vec::new(),
2550            trailing_trivia: Vec::new(),
2551        };
2552
2553        while !self.check(TokenType::RBrace) {
2554            let field_name = self.current().value.clone();
2555            self.advance();
2556            self.consume(TokenType::Colon)?;
2557
2558            match field_name.as_str() {
2559                "store" => node.store = self.consume_any_ident_or_kw()?.value,
2560                "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
2561                "retrieval" => node.retrieval = self.consume_any_ident_or_kw()?.value,
2562                "decay" => {
2563                    if self.check(TokenType::Duration) {
2564                        node.decay = self.advance().value.clone();
2565                    } else {
2566                        node.decay = self.consume_any_ident_or_kw()?.value;
2567                    }
2568                }
2569                _ => self.skip_value(),
2570            }
2571        }
2572        self.consume(TokenType::RBrace)?;
2573        Ok(node)
2574    }
2575
2576    // ── TOOL ─────────────────────────────────────────────────────
2577
2578    fn parse_tool(&mut self) -> Result<ToolDefinition, ParseError> {
2579        let tok = self.consume(TokenType::Tool)?;
2580        let loc = self.loc_of(&tok);
2581        let name = self.consume(TokenType::Identifier)?.value;
2582        self.consume(TokenType::LBrace)?;
2583
2584        let mut node = ToolDefinition {
2585            shield_ref: String::new(),
2586            name,
2587            provider: String::new(),
2588            max_results: None,
2589            filter_expr: String::new(),
2590            timeout: String::new(),
2591            runtime: String::new(),
2592            resource_ref: String::new(),
2593            sandbox: None,
2594            effects: None,
2595            parameters: Vec::new(),
2596            output_type: None,
2597            requires: Vec::new(),
2598            secret: String::new(),
2599            secret_partition: String::new(),
2600            target: None,
2601            risk: None,
2602            argv: Vec::new(),
2603            cache: String::new(),
2604            scrape: None,
2605            loc,
2606            leading_trivia: Vec::new(),
2607            trailing_trivia: Vec::new(),
2608        };
2609
2610        // v2.39.0/the design decision — unknown fields are recorded (not silently
2611        // skipped) so a `target:`-bound technician tool can HARD-ERROR on one
2612        // (a typo'd safety field must never quietly disable a guard), while a
2613        // legacy schema-less tool keeps its lenient record-and-skip (zero
2614        // regression). The decision is deferred to after the block is parsed,
2615        // since `target:` may appear after the unknown field.
2616        let mut unknown_fields: Vec<(String, u32, u32)> = Vec::new();
2617
2618        while !self.check(TokenType::RBrace) {
2619            let field_tok = self.current().clone();
2620            let field_name = field_tok.value.clone();
2621            self.advance();
2622            self.consume(TokenType::Colon)?;
2623
2624            match field_name.as_str() {
2625                "provider" => node.provider = self.consume_any_ident_or_kw()?.value,
2626                "max_results" => {
2627                    node.max_results = Some(
2628                        self.consume(TokenType::Integer)?
2629                            .value
2630                            .parse::<i64>()
2631                            .unwrap_or(0),
2632                    )
2633                }
2634                "filter" => node.filter_expr = self.parse_filter_expression()?,
2635                "timeout" => node.timeout = self.consume(TokenType::Duration)?.value,
2636                "runtime" => node.runtime = self.consume_any_ident_or_kw()?.value,
2637                // v2.69.0 — the `resource` this tool's channel runs on. The
2638                // channel's address, concurrency and lifecycle come from it;
2639                // `runtime:` then names the path within the channel.
2640                "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
2641                "sandbox" => node.sandbox = Some(self.parse_bool()?),
2642                "effects" => node.effects = Some(self.parse_effect_row()?),
2643                // v2.8.0 — the tool's typed input schema + output type.
2644                "parameters" => node.parameters = self.parse_tool_param_schema()?,
2645                "output_type" => node.output_type = Some(self.parse_output_type_string()?),
2646                // v2.77.0 — the tool's required authorization
2647                // scopes: bare dot-separated capability slugs, the EXACT
2648                // grammar + charset of `credential.grants` (v2.46.0) so the two
2649                // vocabularies are one. `requires: [w_organization_social,
2650                // video.publish]`. Subset coverage is `axon-T956`.
2651                "requires" => {
2652                    let bracket_tok = self.current().clone();
2653                    let items = self.parse_bracketed_dot_identifiers()?;
2654                    for slug in &items {
2655                        if !is_valid_capability_slug(slug) {
2656                            return Err(ParseError {
2657                                message: format!(
2658                                    "Invalid capability slug '{slug}' in tool '{}' \
2659                                     `requires:`. Scope slugs must match \
2660                                     ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — the same \
2661                                     grammar as `credential.grants`. Examples: \
2662                                     `w_organization_social`, `video.publish`.",
2663                                    node.name
2664                                ),
2665                                line: bracket_tok.line,
2666                                column: bracket_tok.column,
2667                                ..Default::default()
2668                            });
2669                        }
2670                    }
2671                    node.requires = items;
2672                }
2673                // v2.48.0 — the per-tenant secret KEY injected at
2674                // dispatch (`rotation_without_revelation`). Key shape +
2675                // technician exclusion are `axon-T902` (type-checker).
2676                "secret" => node.secret = self.parse_dotted_identifier()?,
2677                // v4.3.0 — the control that covers this tool's κ (axon-T1221).
2678                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
2679                // v2.49.0 — `secret_partition:` names one of this tool's
2680                // own `parameters:` (a bare identifier, NOT dotted — it is a
2681                // parameter reference, not a key). Its runtime value becomes
2682                // a single appended key segment at dispatch. The membership +
2683                // `String`-type + technician laws are `axon-T903`.
2684                "secret_partition" => {
2685                    node.secret_partition = self.consume_any_ident_or_kw()?.value
2686                }
2687                // v2.39.0 — Remote Hands technician fields.
2688                "target" => node.target = Some(self.consume_any_ident_or_kw()?.value),
2689                "risk" => node.risk = Some(self.consume_any_ident_or_kw()?.value),
2690                // The argv template: a bracketed list of quoted elements
2691                // (`argv: ["ping", "-c", "${count}", "${host}"]`). Reuses the
2692                // CORS list helper (tolerant of `[]` and a trailing comma).
2693                "argv" => node.argv = self.parse_bracketed_strings()?,
2694                // v2.40.0 — the tool's result-memoization policy reference
2695                // (a declared `cache` name, or the `none` opt-out sentinel).
2696                "cache" => node.cache = self.consume_any_ident_or_kw()?.value,
2697                // v2.52.0 — the closed-catalog web-acquisition config
2698                // block. `scrape: { engine: …, extract: […], … }`.
2699                "scrape" => node.scrape = Some(self.parse_scrape_spec()?),
2700                _ => {
2701                    unknown_fields.push((field_name, field_tok.line, field_tok.column));
2702                    self.skip_value();
2703                }
2704            }
2705        }
2706        self.consume(TokenType::RBrace)?;
2707
2708        // v2.39.0/the design decision — a `target:`-bound tool opts into strict field
2709        // checking. An unknown field on it is a parse error, mirroring the v2.38.0
2710        // `cors`/`voice` closed-catalog discipline — but scoped to the
2711        // technician surface so ordinary tools are untouched.
2712        // v2.52.0 — a `scrape:`-bearing web-acquisition tool opts
2713        // into the same strictness: a typo'd safety field (e.g. a mis-spelled
2714        // `respect_robots`) must never quietly disable a guard.
2715        if node.target.is_some() || node.scrape.is_some() {
2716            if let Some((field_name, line, column)) = unknown_fields.into_iter().next() {
2717                let (surface, valid) = if node.target.is_some() {
2718                    (
2719                        "technician tool",
2720                        "provider, parameters, output_type, timeout, effects, target, risk, argv",
2721                    )
2722                } else {
2723                    (
2724                        "web-acquisition tool",
2725                        "provider, parameters, output_type, timeout, effects, secret, \
2726                         secret_partition, cache, scrape",
2727                    )
2728                };
2729                return Err(ParseError {
2730                    message: format!(
2731                        "unknown field `{field_name}` in {surface} `{}` — this tool uses \
2732                         strict field checking; valid fields: {valid}",
2733                        node.name
2734                    ),
2735                    line,
2736                    column,
2737                    ..Default::default()
2738                });
2739            }
2740        }
2741        Ok(node)
2742    }
2743
2744    /// v2.52.0 — parse the closed-catalog `scrape: { … }` web-acquisition
2745    /// config sub-block. Every field is optional; an unknown field is a hard
2746    /// parse error (the v2.38.0 `cors` closed-catalog discipline). Mirrors the
2747    /// field grammar of `parse_tool` for the scrape-specific keys.
2748    fn parse_scrape_spec(&mut self) -> Result<crate::ast::ScrapeSpec, ParseError> {
2749        let open = self.consume(TokenType::LBrace)?;
2750        let loc = self.loc_of(&open);
2751        let mut spec = crate::ast::ScrapeSpec {
2752            loc,
2753            ..Default::default()
2754        };
2755        while !self.check(TokenType::RBrace) {
2756            let field_tok = self.current().clone();
2757            let field_name = field_tok.value.clone();
2758            self.advance();
2759            self.consume(TokenType::Colon)?;
2760            match field_name.as_str() {
2761                "engine" => spec.engine = Some(self.consume_any_ident_or_kw()?.value),
2762                "impersonate" => spec.impersonate = Some(self.consume_any_ident_or_kw()?.value),
2763                "render_wait" => spec.render_wait = Some(self.consume(TokenType::Duration)?.value),
2764                "proxy" => spec.proxy = self.parse_dotted_identifier()?,
2765                "respect_robots" => spec.respect_robots = Some(self.parse_bool()?),
2766                "extract" => spec.extract = self.parse_bracketed_strings()?,
2767                "adaptive" => spec.adaptive = Some(self.parse_bool()?),
2768                "similarity_floor" => spec.similarity_floor = self.parse_optional_float(),
2769                "follow" => spec.follow = self.consume(TokenType::StringLit)?.value,
2770                "max_depth" => {
2771                    spec.max_depth =
2772                        Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2773                }
2774                "max_pages" => {
2775                    spec.max_pages =
2776                        Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2777                }
2778                "concurrency" => {
2779                    spec.concurrency =
2780                        Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2781                }
2782                "politeness" => spec.politeness = self.consume_any_ident_or_kw()?.value,
2783                "checkpoint" => spec.checkpoint = self.consume_any_ident_or_kw()?.value,
2784                other => {
2785                    return Err(self.error(&format!(
2786                        "unknown scrape field `{other}` — the `scrape: {{ … }}` block is a \
2787                         closed catalog; valid fields: engine, impersonate, \
2788                         render_wait, proxy, respect_robots, extract, adaptive, \
2789                         similarity_floor, follow, max_depth, max_pages, concurrency, \
2790                         politeness, checkpoint"
2791                    )));
2792                }
2793            }
2794        }
2795        self.consume(TokenType::RBrace)?;
2796        Ok(spec)
2797    }
2798
2799    /// v2.8.0 — parse a tool's INPUT SCHEMA: a brace-delimited list of
2800    /// `name: Type` parameters (`parameters: { query: String, max_results: Int }`).
2801    /// Reuses the flow-parameter shape (`Parameter`), so the same `TypeExpr`
2802    /// grammar — generics like `List<T>`, `?`-optionals — applies. A trailing
2803    /// comma is tolerated; an empty `{}` yields no parameters.
2804    fn parse_tool_param_schema(&mut self) -> Result<Vec<Parameter>, ParseError> {
2805        self.consume(TokenType::LBrace)?;
2806        let mut params = Vec::new();
2807        while !self.check(TokenType::RBrace) {
2808            // Accept a keyword-as-name (`filter`, `type`, `domain`, …) — real
2809            // adopter tool schemas use such parameter names; the `:` after it
2810            // disambiguates.
2811            let name = self.consume_any_ident_or_kw()?;
2812            let ploc = self.loc_of(&name);
2813            self.consume(TokenType::Colon)?;
2814            let type_expr = self.parse_type_expr()?;
2815            params.push(Parameter {
2816                name: name.value,
2817                type_expr,
2818                loc: ploc,
2819            });
2820            if self.check(TokenType::Comma) {
2821                self.advance();
2822            } else {
2823                break;
2824            }
2825        }
2826        self.consume(TokenType::RBrace)?;
2827        Ok(params)
2828    }
2829
2830    fn parse_filter_expression(&mut self) -> Result<String, ParseError> {
2831        let name = self.consume_any_ident_or_kw()?.value;
2832        if self.check(TokenType::LParen) {
2833            self.advance();
2834            let mut parts = vec![name, "(".to_string()];
2835            while !self.check(TokenType::RParen) {
2836                parts.push(self.advance().value.clone());
2837            }
2838            self.consume(TokenType::RParen)?;
2839            parts.push(")".to_string());
2840            Ok(parts.join(""))
2841        } else {
2842            Ok(name)
2843        }
2844    }
2845
2846    fn parse_effect_row(&mut self) -> Result<EffectRow, ParseError> {
2847        let tok = self.consume(TokenType::Lt)?;
2848        let loc = self.loc_of(&tok);
2849        let mut effects = Vec::new();
2850        let mut epistemic_level = String::new();
2851
2852        while !self.check(TokenType::Gt) {
2853            let name = self.consume_any_ident_or_kw()?.value;
2854            if self.check(TokenType::Colon) {
2855                self.advance();
2856                // v1.4.0 — qualifiers can be compound slugs
2857                // from a closed catalogue:
2858                //
2859                //   * dot-separated  — `legal:HIPAA.164_502`,
2860                //                       `legal:GDPR.Art6.Consent`,
2861                //                       `legal:PCI_DSS.v4_Req3`
2862                //   * colon-separated — `ots:transform:mulaw8:pcm16`,
2863                //                       `ots:backend:native`
2864                //   * mixed           — supported by the same loop.
2865                //
2866                // The lexer fragments dotted slugs across IDENT /
2867                // INTEGER tokens (e.g., `164_502` lexes as INTEGER
2868                // `164` + IDENT `_502` because `_` starts a fresh
2869                // identifier); we recombine here using source-column
2870                // adjacency so the type checker sees the catalog
2871                // string verbatim.
2872                let level = self.parse_qualifier_value()?;
2873                if name == "epistemic" {
2874                    epistemic_level = level;
2875                } else {
2876                    effects.push(format!("{name}:{level}"));
2877                }
2878            } else {
2879                effects.push(name);
2880            }
2881            if self.check(TokenType::Comma) {
2882                self.advance();
2883            }
2884        }
2885        self.consume(TokenType::Gt)?;
2886
2887        Ok(EffectRow {
2888            effects,
2889            epistemic_level,
2890            loc,
2891        })
2892    }
2893
2894    /// Parse a compound qualifier value following an effect's first
2895    /// colon — supports both dot-separated (`HIPAA.164_502`) and
2896    /// colon-separated (`transform:mulaw8:pcm16`) catalogue slugs, as
2897    /// well as mixed forms.
2898    ///
2899    /// The grammar is: `segment ((`.` | `:`) segment)*` where a
2900    /// segment is a contiguous run of IDENT / INTEGER tokens (see
2901    /// [`Self::consume_dotted_slug_segment`]).
2902    fn parse_qualifier_value(&mut self) -> Result<String, ParseError> {
2903        let mut buf = self.consume_dotted_slug_segment()?;
2904        loop {
2905            let sep = if self.check(TokenType::Dot) {
2906                '.'
2907            } else if self.check(TokenType::Colon) {
2908                ':'
2909            } else {
2910                break;
2911            };
2912            self.advance();
2913            let part = self.consume_dotted_slug_segment()?;
2914            buf.push(sep);
2915            buf.push_str(&part);
2916        }
2917        Ok(buf)
2918    }
2919
2920    /// Consume a contiguous run of IDENT / INTEGER / keyword-ident
2921    /// tokens whose source positions are adjacent (no whitespace
2922    /// between them), concatenating their text into a single segment.
2923    ///
2924    /// Needed for closed-catalogue qualifier slugs whose segment
2925    /// mixes digits and identifier characters — e.g. `HIPAA.164_502`
2926    /// lexes as INTEGER `164` + IDENT `_502` because `_` starts a
2927    /// fresh identifier; the catalog value is the concatenation
2928    /// `164_502`. Adjacency is determined by matching
2929    /// `(line, column + len)` of the previous token against the next
2930    /// token's start position.
2931    fn consume_dotted_slug_segment(&mut self) -> Result<String, ParseError> {
2932        let first = self.consume_any_ident_or_kw()?;
2933        let mut buf = first.value.clone();
2934        let mut next_line = first.line;
2935        let mut next_col = first.column + first.value.chars().count() as u32;
2936        loop {
2937            let cur = self.current();
2938            let is_segment_token = matches!(cur.ttype, TokenType::Identifier | TokenType::Integer,);
2939            if !is_segment_token {
2940                break;
2941            }
2942            if cur.line != next_line || cur.column != next_col {
2943                break;
2944            }
2945            buf.push_str(&cur.value);
2946            next_col = cur.column + cur.value.chars().count() as u32;
2947            next_line = cur.line;
2948            self.pos += 1;
2949        }
2950        Ok(buf)
2951    }
2952
2953    // ── TYPE ─────────────────────────────────────────────────────
2954
2955    fn parse_type_def(&mut self) -> Result<TypeDefinition, ParseError> {
2956        let tok = self.consume(TokenType::Type)?;
2957        let loc = self.loc_of(&tok);
2958        let name = self.consume(TokenType::Identifier)?.value;
2959
2960        let mut node = TypeDefinition {
2961            name,
2962            fields: Vec::new(),
2963            range_constraint: None,
2964            where_clause: None,
2965            compliance: Vec::new(),
2966            loc: loc.clone(),
2967            leading_trivia: Vec::new(),
2968            trailing_trivia: Vec::new(),
2969        };
2970
2971        // Optional range: (0.0..1.0)
2972        if self.check(TokenType::LParen) {
2973            self.advance();
2974            let min_val = self.consume_number()?;
2975            self.consume(TokenType::DotDot)?;
2976            let max_val = self.consume_number()?;
2977            self.consume(TokenType::RParen)?;
2978            node.range_constraint = Some(RangeConstraint {
2979                min_value: min_val,
2980                max_value: max_val,
2981                loc: loc.clone(),
2982            });
2983        }
2984
2985        // Optional where clause
2986        if self.check(TokenType::Where) {
2987            self.advance();
2988            let mut expr_parts = Vec::new();
2989            while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
2990                if self.check(TokenType::Eof) {
2991                    break;
2992                }
2993                expr_parts.push(self.advance().value.clone());
2994            }
2995            node.where_clause = Some(WhereClause {
2996                expression: expr_parts.join(" "),
2997                loc: loc.clone(),
2998            });
2999        }
3000
3001        // Optional ESK — `compliance [HIPAA, ...]` prefix modifier
3002        // between `type Name` / `range` / `where` and the body `{`.
3003        if self.check(TokenType::Identifier) && self.current().value == "compliance" {
3004            self.advance();
3005            node.compliance = self.parse_bracketed_identifiers()?;
3006        }
3007
3008        // Optional body: { field: Type, ... }
3009        if self.check(TokenType::LBrace) {
3010            self.advance();
3011            while !self.check(TokenType::RBrace) {
3012                let field_name = self.consume(TokenType::Identifier)?;
3013                let field_loc = self.loc_of(&field_name);
3014                self.consume(TokenType::Colon)?;
3015                let type_expr = self.parse_type_expr()?;
3016                node.fields.push(TypeField {
3017                    name: field_name.value,
3018                    type_expr,
3019                    loc: field_loc,
3020                });
3021                if self.check(TokenType::Comma) {
3022                    self.advance();
3023                }
3024            }
3025            self.consume(TokenType::RBrace)?;
3026        }
3027
3028        Ok(node)
3029    }
3030
3031    fn parse_type_expr(&mut self) -> Result<TypeExpr, ParseError> {
3032        // v2.83.0 — a LEADING bracket is the list-type sugar the README
3033        // has always written in flow signatures: `readings: [SensorReading]`
3034        // (blocks 44-45). It lowers to exactly what `List<SensorReading>`
3035        // produces, so nothing downstream learns a new shape — the v2.0.0
3036        // comment below already names `List<T>` as the canonical carrier.
3037        if self.check(TokenType::LBracket) {
3038            let open = self.current().clone();
3039            self.advance();
3040            let inner = self.parse_type_expr()?;
3041            self.consume(TokenType::RBracket)?;
3042            let mut optional = false;
3043            if self.check(TokenType::Question) {
3044                self.advance();
3045                optional = true;
3046            }
3047            return Ok(TypeExpr {
3048                name: "List".to_string(),
3049                generic_param: if inner.generic_param.is_empty() {
3050                    inner.name
3051                } else {
3052                    format!("{}<{}>", inner.name, inner.generic_param)
3053                },
3054                optional,
3055                loc: self.loc_of(&open),
3056            });
3057        }
3058        let name_tok = self.consume(TokenType::Identifier)?;
3059        let loc = self.loc_of(&name_tok);
3060        let mut generic_param = String::new();
3061        let mut optional = false;
3062
3063        if self.check(TokenType::Lt) {
3064            self.advance();
3065            // v2.0.0 — recursive: the generic param can itself be a
3066            // nested type expression. `FlowEnvelope<List<TenantRecord>>`
3067            // parses as outer=FlowEnvelope, inner=List<TenantRecord>.
3068            // Pre-39.a the inner had to be a single Identifier; nested
3069            // generics like the canonical FlowEnvelope<T> wrapper
3070            // required this lift. Backwards-compat preserved for
3071            // single-level generics like `Stream<Token>` and
3072            // `List<T>` — the recursion lands once and returns the
3073            // same flat string the v1.x parser produced.
3074            let inner = self.parse_type_expr()?;
3075            generic_param = if inner.generic_param.is_empty() {
3076                inner.name
3077            } else {
3078                format!("{}<{}>", inner.name, inner.generic_param)
3079            };
3080            self.consume(TokenType::Gt)?;
3081        }
3082        // v2.4.0 — bracket type parameters for the continuous-carrier
3083        // grammar: `SymbolicPtr[Tensor[Float32]]`, `DensityMatrix[1024]`. The
3084        // param is either a nested type expression OR a numeric dimension.
3085        if self.check(TokenType::LBracket) {
3086            self.advance();
3087            if matches!(self.current().ttype, TokenType::Integer | TokenType::Float) {
3088                generic_param = self.advance().value.clone();
3089            } else {
3090                let inner = self.parse_type_expr()?;
3091                generic_param = if inner.generic_param.is_empty() {
3092                    inner.name
3093                } else {
3094                    format!("{}[{}]", inner.name, inner.generic_param)
3095                };
3096            }
3097            self.consume(TokenType::RBracket)?;
3098        }
3099        if self.check(TokenType::Question) {
3100            self.advance();
3101            optional = true;
3102        }
3103
3104        Ok(TypeExpr {
3105            name: name_tok.value,
3106            generic_param,
3107            optional,
3108            loc,
3109        })
3110    }
3111
3112    /// Parse a type expression in a context where the AST stores the
3113    /// shape as a flat string (step / reason / forge / ots-apply
3114    /// productions). Mirrors Python `_parse_output_type_string`.
3115    ///
3116    /// Accepts:
3117    /// - `Identifier`        → `"Identifier"`
3118    /// - `Stream<String>`    → `"Stream<String>"`
3119    /// - `Optional?`         → `"Optional?"`
3120    /// - `Stream<String>?`   → `"Stream<String>?"`
3121    ///
3122    /// **Why this exists** — pre-fix, the step parser called
3123    /// `consume(TokenType::Identifier)?.value` which captured only
3124    /// the head identifier and left `< … >` unconsumed. For
3125    /// `output: Stream<Token>`, this produced `output_type =
3126    /// "Stream"`, and downstream `flow_has_stream_output`'s
3127    /// `starts_with("Stream<") && ends_with('>')` predicate then
3128    /// returned false → `implicit_transport == "json"` → the
3129    /// dynamic-route fallback in `axon-rs` served JSON instead of
3130    /// SSE even when the adopter's source canonically declared the
3131    /// algebraic stream effect. Surfaced 2026-05-12 by adopter
3132    /// `docs/MIGRATION_TO_AXON.md` audit after the v1.23.0 wire-
3133    /// layer didn't honor the declarative effect. Python parser was
3134    /// fixed for the same gap 2026-05-09; this is the Rust cross-
3135    /// stack catch-up.
3136    fn parse_output_type_string(&mut self) -> Result<String, ParseError> {
3137        let expr = self.parse_type_expr()?;
3138        let mut s = expr.name;
3139        if !expr.generic_param.is_empty() {
3140            s.push('<');
3141            s.push_str(&expr.generic_param);
3142            s.push('>');
3143        }
3144        if expr.optional {
3145            s.push('?');
3146        }
3147        Ok(s)
3148    }
3149
3150    // ── FLOW ─────────────────────────────────────────────────────
3151
3152    fn parse_flow(&mut self) -> Result<FlowDefinition, ParseError> {
3153        let tok = self.consume(TokenType::Flow)?;
3154        let loc = self.loc_of(&tok);
3155        let name = self.consume(TokenType::Identifier)?.value;
3156
3157        self.consume(TokenType::LParen)?;
3158        let mut parameters = Vec::new();
3159        if !self.check(TokenType::RParen) {
3160            parameters = self.parse_param_list()?;
3161        }
3162        self.consume(TokenType::RParen)?;
3163
3164        let mut return_type = None;
3165        if self.check(TokenType::Arrow) {
3166            self.advance();
3167            return_type = Some(self.parse_type_expr()?);
3168        }
3169
3170        self.consume(TokenType::LBrace)?;
3171        let mut body = Vec::new();
3172        while !self.check(TokenType::RBrace) {
3173            body.push(self.parse_flow_step()?);
3174        }
3175        self.consume(TokenType::RBrace)?;
3176
3177        Ok(FlowDefinition {
3178            name,
3179            parameters,
3180            return_type,
3181            body,
3182            loc,
3183            leading_trivia: Vec::new(),
3184            trailing_trivia: Vec::new(),
3185        })
3186    }
3187
3188    // ── v2.87.0 — algebraic effects (Plotkin/Pretnar) ─────────────
3189    //
3190    // Four constructs, in the shape `the design plan` section 3.1 publishes verbatim.
3191
3192    /// `effect SSE { Emit(token: Token) -> Unit  Done() -> Never }`
3193    ///
3194    /// The declaration exists so the operation catalog is CLOSED. the design decision's bare
3195    /// `perform Emit(x)` resolves against exactly this set, and an operation
3196    /// two effects both declare is a compile error naming both — not a silent
3197    /// pick. Without the declaration there would be nothing to resolve against
3198    /// and `effect_name` would be a free string, which is the defect
3199    /// `feedback_free_string_field_breeds_fake_catalog` names.
3200    fn parse_effect(&mut self) -> Result<EffectDefinition, ParseError> {
3201        let tok = self.consume(TokenType::Effect)?;
3202        let loc = self.loc_of(&tok);
3203        let name = self.consume_any_ident_or_kw()?.value;
3204        self.consume(TokenType::LBrace)?;
3205
3206        let mut operations: Vec<EffectOperation> = Vec::new();
3207        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3208            let op_tok = self.current().clone();
3209            let op_name = self.consume_any_ident_or_kw()?.value;
3210
3211            self.consume(TokenType::LParen)?;
3212            let parameters = if self.check(TokenType::RParen) {
3213                Vec::new()
3214            } else {
3215                self.parse_param_list()?
3216            };
3217            self.consume(TokenType::RParen)?;
3218
3219            // `-> T` is optional in the grammar; section 3.1 always writes it, and a
3220            // missing return type reads as Unit at the type-checker.
3221            let mut return_type = String::new();
3222            if self.check(TokenType::Arrow) {
3223                self.advance();
3224                return_type = self.parse_type_expr()?.name;
3225            }
3226
3227            // A duplicate operation name inside ONE effect is refused: the
3228            // handler-clause lookup is by operation name, so two declarations
3229            // would make the arity check depend on which one the search found
3230            // first — a defect nobody would ever see fire.
3231            if let Some(prior) = operations.iter().find(|o| o.name == op_name) {
3232                return Err(ParseError {
3233                    message: format!(
3234                        "effect `{name}` declares operation `{op_name}` twice (first at \
3235                         line {}); handler dispatch is by operation NAME, so a second \
3236                         declaration would silently shadow the first",
3237                        prior.loc.line
3238                    ),
3239                    line: op_tok.line,
3240                    column: op_tok.column,
3241                    ..Default::default()
3242                });
3243            }
3244
3245            operations.push(EffectOperation {
3246                name: op_name,
3247                parameters,
3248                return_type,
3249                loc: self.loc_of(&op_tok),
3250            });
3251        }
3252        self.consume(TokenType::RBrace)?;
3253
3254        Ok(EffectDefinition {
3255            name,
3256            operations,
3257            loc,
3258            leading_trivia: Vec::new(),
3259            trailing_trivia: Vec::new(),
3260        })
3261    }
3262
3263    /// `handle SSE { Emit(token) -> { … } } in { … }` — the delimited handler
3264    /// scope (D3).
3265    ///
3266    /// The `in { … }` body is parsed with [`Self::parse_flow_step`], and that is
3267    /// the whole point of the design decision: the body is ORDINARY flow steps, so
3268    /// `run generate(…)` inside a handler runs for real. Lowering it onto
3269    /// `axon-rs`'s `Instruction` alphabet instead would have made every
3270    /// non-effect node in it a `Passthrough` — inert — which is the v2.67.0 defect
3271    /// this cycle exists not to repeat.
3272    fn parse_handle_block(&mut self) -> Result<HandleBlock, ParseError> {
3273        let tok = self.consume(TokenType::Handle)?;
3274        let loc = self.loc_of(&tok);
3275
3276        // `handle E1, E2 { … }` — one frame may intercept several effects.
3277        let mut effect_names = vec![self.consume_any_ident_or_kw()?.value];
3278        while self.check(TokenType::Comma) {
3279            self.advance();
3280            effect_names.push(self.consume_any_ident_or_kw()?.value);
3281        }
3282
3283        self.consume(TokenType::LBrace)?;
3284        let mut clauses: Vec<HandlerClause> = Vec::new();
3285        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3286            let clause_tok = self.current().clone();
3287            let operation_name = self.consume_any_ident_or_kw()?.value;
3288
3289            // Clause binders are BARE names — `Emit(token) -> { … }`. The types
3290            // live on the effect declaration; repeating them here would let the
3291            // two disagree.
3292            self.consume(TokenType::LParen)?;
3293            let mut parameter_names = Vec::new();
3294            while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
3295                parameter_names.push(self.consume_any_ident_or_kw()?.value);
3296                if self.check(TokenType::Comma) {
3297                    self.advance();
3298                }
3299            }
3300            self.consume(TokenType::RParen)?;
3301            self.consume(TokenType::Arrow)?;
3302            self.consume(TokenType::LBrace)?;
3303
3304            let mut body = Vec::new();
3305            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3306                body.push(self.parse_flow_step()?);
3307            }
3308            self.consume(TokenType::RBrace)?;
3309
3310            if let Some(prior) = clauses.iter().find(|c| c.operation_name == operation_name) {
3311                return Err(ParseError {
3312                    message: format!(
3313                        "handler declares clause `{operation_name}` twice (first at line {}); \
3314                         dispatch finds a clause by operation NAME and would always run the \
3315                         first, leaving the second dead",
3316                        prior.loc.line
3317                    ),
3318                    line: clause_tok.line,
3319                    column: clause_tok.column,
3320                    ..Default::default()
3321                });
3322            }
3323
3324            clauses.push(HandlerClause {
3325                operation_name,
3326                parameter_names,
3327                body,
3328                loc: self.loc_of(&clause_tok),
3329            });
3330        }
3331        self.consume(TokenType::RBrace)?;
3332
3333        // The `in { … }` delimiter is MANDATORY. A `handle` without it declares
3334        // a scope with no extent — nothing could ever be intercepted by it, and
3335        // accepting it would let an author believe an effect was handled when
3336        // no `perform` is inside anything.
3337        let in_tok = self.current().clone();
3338        if !self.check(TokenType::In) {
3339            return Err(ParseError {
3340                message: format!(
3341                    "`handle {}` must be followed by `in {{ … }}` — a handler scope is \
3342                     DELIMITED (the design plan D3). Without the `in` block the frame has no \
3343                     extent, so no `perform` could ever reach these clauses (got '{}')",
3344                    effect_names.join(", "),
3345                    in_tok.value
3346                ),
3347                line: in_tok.line,
3348                column: in_tok.column,
3349                ..Default::default()
3350            });
3351        }
3352        self.advance();
3353        self.consume(TokenType::LBrace)?;
3354        let mut body = Vec::new();
3355        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3356            body.push(self.parse_flow_step()?);
3357        }
3358        self.consume(TokenType::RBrace)?;
3359
3360        Ok(HandleBlock {
3361            effect_names,
3362            clauses,
3363            body,
3364            loc,
3365        })
3366    }
3367
3368    /// The shared head of `perform` and `forward` (D12): an optionally
3369    /// qualified operation name plus a parenthesised argument list.
3370    ///
3371    /// the design decision — BOTH spellings parse. `SSE.Emit(x)` fixes the effect here;
3372    /// `Emit(x)` leaves `effect_name` `None` and the closed catalog resolves it
3373    /// downstream, where an ambiguity can be reported with both candidates
3374    /// named. The qualified form is told from the bare one by the `.`, which
3375    /// cannot appear in an operation name.
3376    fn parse_effect_op_ref(
3377        &mut self,
3378    ) -> Result<(Option<String>, String, Vec<String>), ParseError> {
3379        let first = self.consume_any_ident_or_kw()?.value;
3380        let (effect_name, operation_name) = if self.check(TokenType::Dot) {
3381            self.advance();
3382            (Some(first), self.consume_any_ident_or_kw()?.value)
3383        } else {
3384            (None, first)
3385        };
3386
3387        self.consume(TokenType::LParen)?;
3388        let mut arguments = Vec::new();
3389        while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
3390            // v2.83.0 SUBJECTS: `the design plan` section 3.1 writes `perform
3391            // Emit(response.token)` — a dotted reference into a prior binding.
3392            arguments.push(self.parse_subject()?);
3393            if self.check(TokenType::Comma) {
3394                self.advance();
3395            }
3396        }
3397        self.consume(TokenType::RParen)?;
3398        Ok((effect_name, operation_name, arguments))
3399    }
3400
3401    /// `perform Emit(x)` / `perform SSE.Emit(x)`.
3402    fn parse_perform_step(&mut self) -> Result<PerformStep, ParseError> {
3403        let tok = self.consume(TokenType::Perform)?;
3404        let (effect_name, operation_name, arguments) = self.parse_effect_op_ref()?;
3405        Ok(PerformStep {
3406            effect_name,
3407            operation_name,
3408            arguments,
3409            loc: self.loc_of(&tok),
3410        })
3411    }
3412
3413    /// `forward Emit(t)` / `forward SSE.Emit(t)` (D12).
3414    fn parse_forward_step(&mut self) -> Result<ForwardStep, ParseError> {
3415        let tok = self.consume(TokenType::Forward)?;
3416        let (effect_name, operation_name, arguments) = self.parse_effect_op_ref()?;
3417        Ok(ForwardStep {
3418            effect_name,
3419            operation_name,
3420            arguments,
3421            loc: self.loc_of(&tok),
3422        })
3423    }
3424
3425    /// The shared body of `resume(…)` / `abort(…)`: an optional single value.
3426    fn parse_discharge_value(&mut self) -> Result<String, ParseError> {
3427        self.consume(TokenType::LParen)?;
3428        let value = if self.check(TokenType::RParen) {
3429            String::new()
3430        } else {
3431            self.parse_subject()?
3432        };
3433        self.consume(TokenType::RParen)?;
3434        Ok(value)
3435    }
3436
3437    fn parse_param_list(&mut self) -> Result<Vec<Parameter>, ParseError> {
3438        let mut params = Vec::new();
3439
3440        let name = self.consume(TokenType::Identifier)?;
3441        let ploc = self.loc_of(&name);
3442        self.consume(TokenType::Colon)?;
3443        let type_expr = self.parse_type_expr()?;
3444        params.push(Parameter {
3445            name: name.value,
3446            type_expr,
3447            loc: ploc,
3448        });
3449
3450        while self.check(TokenType::Comma) {
3451            self.advance();
3452            let name = self.consume(TokenType::Identifier)?;
3453            let ploc = self.loc_of(&name);
3454            self.consume(TokenType::Colon)?;
3455            let type_expr = self.parse_type_expr()?;
3456            params.push(Parameter {
3457                name: name.value,
3458                type_expr,
3459                loc: ploc,
3460            });
3461        }
3462        Ok(params)
3463    }
3464
3465    // ── FLOW STEP dispatch ───────────────────────────────────────
3466
3467    fn parse_flow_step(&mut self) -> Result<FlowStep, ParseError> {
3468        let tok = self.current().clone();
3469
3470        match tok.ttype {
3471            // v2.83.0 — an epistemic block INSIDE a flow body. Its
3472            // children are hoisted to program level (see `Parser::hoisted`),
3473            // which is exactly what a top-level block already does, so the
3474            // nested spelling costs nothing downstream. The flow itself gets
3475            // no node: the block declares, it does not execute.
3476            TokenType::Know | TokenType::Believe | TokenType::Speculate
3477                if self
3478                    .tokens
3479                    .get(self.pos + 1)
3480                    .is_some_and(|t| t.ttype == TokenType::LBrace) =>
3481            {
3482                let block = self.parse_epistemic_block()?;
3483                self.hoisted.push(Declaration::Epistemic(block));
3484                self.parse_flow_step()
3485            }
3486            TokenType::Doubt
3487                if self
3488                    .tokens
3489                    .get(self.pos + 1)
3490                    .is_some_and(|t| t.ttype == TokenType::LBrace) =>
3491            {
3492                let block = self.parse_epistemic_block()?;
3493                self.hoisted.push(Declaration::Epistemic(block));
3494                self.parse_flow_step()
3495            }
3496            TokenType::Step => self.parse_step().map(FlowStep::Step),
3497            TokenType::If => self.parse_if().map(FlowStep::If),
3498            TokenType::For => self.parse_for_in().map(FlowStep::ForIn),
3499            TokenType::Let => self.parse_let().map(FlowStep::Let),
3500            TokenType::Return => self.parse_return().map(FlowStep::Return),
3501            TokenType::Break => self.parse_break().map(FlowStep::Break),
3502            TokenType::Continue => self.parse_continue().map(FlowStep::Continue),
3503            TokenType::Lambda => self.parse_lambda_data_apply().map(FlowStep::LambdaDataApply),
3504
3505            // ── Tier 2 flow steps (typed AST) ─────────────────────
3506            TokenType::Probe => self.parse_flow_step_simple("probe").map(|l| FlowStep::Probe(ProbeStep { target: l.1, fields: Vec::new(), loc: l.0 })),
3507            // v2.83.0 — ONE implementation for both positions (the the design decision
3508            // doctrine). `reason <target>` and `reason { given ask depth }` are
3509            // the same node; the second is what the README publishes.
3510            TokenType::Reason => self.parse_reason_step().map(FlowStep::Reason),
3511            TokenType::Validate => self.parse_flow_step_simple("validate").map(|l| FlowStep::Validate(ValidateStep { target: l.1, rule: String::new(), guard: None, loc: l.0 })),
3512            TokenType::Refine => self.parse_flow_step_simple("refine").map(|l| FlowStep::Refine(RefineStep { target: l.1, strategy: String::new(), loc: l.0 })),
3513            TokenType::Weave => self.parse_weave_step(),
3514            TokenType::Use => self.parse_use_step(),
3515            TokenType::Remember => self.parse_remember_step(),
3516            TokenType::Recall => self.parse_recall_step(),
3517            TokenType::Par => self.parse_par_block().map(FlowStep::Par),
3518            TokenType::Hibernate => self.parse_hibernate_step(),
3519            TokenType::Deliberate => self.parse_block_step("deliberate").map(|l| FlowStep::Deliberate(DeliberateBlock { loc: l })),
3520            TokenType::Consensus => self.parse_block_step("consensus").map(|l| FlowStep::Consensus(ConsensusBlock { loc: l })),
3521            TokenType::Forge => self.parse_forge_step().map(FlowStep::Forge),
3522            TokenType::Focus => self.parse_focus_step(),
3523            TokenType::Grad => self.parse_grad_step(),
3524            TokenType::Associate => self.parse_associate_step(),
3525            TokenType::Aggregate => self.parse_aggregate_step(),
3526            TokenType::Explore => self.parse_explore_step(),
3527            TokenType::Ingest => self.parse_ingest_step(),
3528            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 })),
3529            // v2.67.0 — `stream` parses its BODY. It used to go through
3530            // `parse_block_step`, whose entire job is `skip_braced_block()` —
3531            // the block's contents were thrown away at parse time, which is why
3532            // `run_stream` had nothing to run and "completed" with an empty
3533            // string while the README sold "Algebraic Effects and Free Monads".
3534            TokenType::Stream => self.parse_stream_block().map(FlowStep::Stream),
3535            // ── v2.87.0 — algebraic effects ────────────────────
3536            //
3537            // All five constructs parse at flow level. `resume` / `abort` /
3538            // `forward` are legal only inside a handler CLAUSE — that scope law
3539            // is enforced by the type-checker (v2.87.0), not here, because the
3540            // parser does not know whether an enclosing `handle` exists when it
3541            // is re-entered through `parse_flow_step` from a clause body.
3542            TokenType::Handle => self.parse_handle_block().map(FlowStep::Handle),
3543            TokenType::Perform => self.parse_perform_step().map(FlowStep::Perform),
3544            TokenType::Resume => {
3545                let tok = self.consume(TokenType::Resume)?;
3546                let value_expr = self.parse_discharge_value()?;
3547                Ok(FlowStep::Resume(ResumeStep {
3548                    value_expr,
3549                    loc: self.loc_of(&tok),
3550                }))
3551            }
3552            TokenType::Abort => {
3553                let tok = self.consume(TokenType::Abort)?;
3554                let value_expr = self.parse_discharge_value()?;
3555                Ok(FlowStep::Abort(AbortStep {
3556                    value_expr,
3557                    loc: self.loc_of(&tok),
3558                }))
3559            }
3560            TokenType::Forward => self.parse_forward_step().map(FlowStep::Forward),
3561            TokenType::Navigate => self.parse_navigate_step(),
3562            TokenType::Drill => self.parse_drill_step(),
3563            TokenType::Trail => self.parse_flow_step_simple("trail").map(|l| FlowStep::Trail(TrailStep { navigate_ref: l.1, loc: l.0 })),
3564            TokenType::Corroborate => self.parse_corroborate_step(),
3565            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 })),
3566            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 })),
3567            // v2.67.0 — `compute <Name> on a, b -> out`. The ARGUMENTS used to
3568            // be `Vec::new()` — hardcoded empty at the parse site — so even if
3569            // the runtime had wanted to compute something, it had nothing to
3570            // compute it FROM.
3571            TokenType::Compute => self.parse_compute_apply().map(FlowStep::ComputeApply),
3572            TokenType::Listen => self.parse_listen_step(),
3573            TokenType::Daemon => self.parse_flow_step_simple("daemon").map(|l| FlowStep::DaemonStep(DaemonStepNode { daemon_ref: l.1, loc: l.0 })),
3574            // v1.6.0 — Mobile typed channels (paper section 3.1, section 3.2, section 4.3)
3575            TokenType::Emit => self.parse_emit_step(),
3576            // v2.46.0 — `mint <Credential> as <binding>` (ephemeral credential).
3577            TokenType::Mint => self.parse_mint_step(),
3578            // v2.48.0 — `rotate <SecretsStore> [where "…"] with <Tool> as
3579            // <binding>` (mediated secret renewal).
3580            TokenType::Rotate => self.parse_rotate_step(),
3581            TokenType::Publish => self.parse_publish_step(),
3582            TokenType::Discover => self.parse_discover_step(),
3583            TokenType::Persist => self.parse_persist_step(),
3584            TokenType::Retrieve => self.parse_retrieve_step(),
3585            TokenType::Mutate => self.parse_mutate_step(),
3586            TokenType::Purge => self.parse_store_where_step().map(|(loc, store_name, where_expr)| FlowStep::Purge(PurgeStep { store_name, where_expr, loc })),
3587            TokenType::Transact => self.parse_block_step("transact").map(|l| FlowStep::Transact(TransactBlock { loc: l })),
3588            // v2.43.0 — the `warden` adversarial-analysis block.
3589            TokenType::Warden => self.parse_warden().map(FlowStep::Warden),
3590            // v2.4.0 — the `quant` cognitive block (Hilbert-space projection).
3591            TokenType::Quant => self.parse_quant().map(FlowStep::Quant),
3592            // v2.4.0 — the `yield` measurement point.
3593            TokenType::Yield => self.parse_yield().map(FlowStep::Yield),
3594            // v2.4.0 — `run <Flow>(args)` as a flow-step: invoke a declared
3595            // flow from inside a body (a `daemon` listen handler, Q3). Reuses
3596            // the top-level run parser.
3597            TokenType::Run => self.parse_run().map(FlowStep::Run),
3598
3599            _ => {
3600                // v1.20.0 — append "Did you mean X?" hint when the
3601                // unknown token looks like a typo'd flow-body keyword
3602                // (e.g. `stepp` / `reasn` / `validte`). D3, D11.
3603                let hint = crate::smart_suggest::suggest_for(
3604                    &tok.value,
3605                    crate::smart_suggest::FLOW_BODY_KEYWORD_NAMES,
3606                );
3607                let base = format!(
3608                    "Unexpected token in flow body: '{}' — expected step, if, for, let, return, ...",
3609                    tok.value
3610                );
3611                let message = if hint.is_empty() {
3612                    base
3613                } else {
3614                    format!("{base}. {hint}")
3615                };
3616                Err(ParseError {
3617                    message,
3618                    line: tok.line,
3619                    column: tok.column,
3620                    ..Default::default()
3621                })
3622            }
3623        }
3624    }
3625
3626    // ── STEP ─────────────────────────────────────────────────────
3627
3628    fn parse_step(&mut self) -> Result<StepNode, ParseError> {
3629        let tok = self.consume(TokenType::Step)?;
3630        let loc = self.loc_of(&tok);
3631        let name = self.consume(TokenType::Identifier)?.value;
3632
3633        let mut persona_ref = String::new();
3634        if self.check(TokenType::Use) {
3635            self.advance();
3636            persona_ref = self.consume_any_ident_or_kw()?.value;
3637        }
3638
3639        self.consume(TokenType::LBrace)?;
3640
3641        let mut node = StepNode {
3642            name,
3643            persona_ref,
3644            given: String::new(),
3645            ask: String::new(),
3646            output_type: String::new(),
3647            confidence_floor: None,
3648            navigate_ref: String::new(),
3649            apply_ref: String::new(),
3650            requires_context: None,
3651            now_tz: None,
3652            guards: Vec::new(),
3653            pix_ops: Vec::new(),
3654            stream: None,
3655            performs: Vec::new(),
3656            loc,
3657        };
3658
3659        self.parse_step_body_into(&mut node)?;
3660        self.consume(TokenType::RBrace)?;
3661        Ok(node)
3662    }
3663
3664    /// v2.83.0 — the step-body field/statement loop, extracted from
3665    /// [`Self::parse_step`] so a `stream<T>` handler arm can reuse it VERBATIM.
3666    ///
3667    /// The caller has already consumed the opening `{` and owns the closing `}`.
3668    ///
3669    /// Extracting it is what keeps `on_chunk: { … }` honest. The published arm
3670    /// body is a STEP body — `probe chunk for […]` followed by
3671    /// `output: QuoteSnapshot` — and `output:` has no flow-level position, so
3672    /// parsing the arm as a flow body would have rejected the README's own
3673    /// example. Re-implementing the loop instead would fork the grammar: every
3674    /// future step-body statement would have to be added twice, and the second
3675    /// copy is the one that rots.
3676    fn parse_step_body_into(&mut self, node: &mut StepNode) -> Result<(), ParseError> {
3677        while !self.check(TokenType::RBrace) {
3678            let inner = self.current().clone();
3679
3680            match inner.ttype {
3681                TokenType::Given => {
3682                    self.advance();
3683                    self.consume(TokenType::Colon)?;
3684                    node.given = self.parse_expression_string()?;
3685                }
3686                TokenType::Ask => {
3687                    self.advance();
3688                    self.consume(TokenType::Colon)?;
3689                    node.ask = self.consume(TokenType::StringLit)?.value;
3690                }
3691                TokenType::Output => {
3692                    // Mirror of Python `_parse_step` `case "output":`
3693                    // which uses `_parse_output_type_string` — accepts
3694                    // the FULL generic-aware shape `Stream<T>`,
3695                    // `Stream<T>?`, `Identifier?`, NOT just the bare
3696                    // head identifier. Pre-fix the step parser dropped
3697                    // `<T>` and downstream `flow_has_stream_output`'s
3698                    // `starts_with("Stream<") && ends_with('>')` then
3699                    // returned false → `implicit_transport == "json"`
3700                    // → dynamic routes served JSON instead of SSE.
3701                    self.advance();
3702                    self.consume(TokenType::Colon)?;
3703                    node.output_type = self.parse_output_type_string()?;
3704                }
3705                // v2.83.0 — `navigate` in a step body is TWO forms, told
3706                // apart by the token after the keyword:
3707                // `navigate: <Ref>` the field (pre-v2.83.0)
3708                //   `navigate <Ref> query: …` the STATEMENT README publishes
3709                // The second is an elevation: it binds `as:` before the step
3710                // generates, so the step's `ask:` can interpolate it.
3711                TokenType::Navigate
3712                    if self
3713                        .tokens
3714                        .get(self.pos + 1)
3715                        .is_some_and(|t| t.ttype != TokenType::Colon) =>
3716                {
3717                    let op = self.parse_navigate_step()?;
3718                    node.pix_ops.push(op);
3719                }
3720                TokenType::Drill => {
3721                    let op = self.parse_drill_step()?;
3722                    node.pix_ops.push(op);
3723                }
3724                TokenType::Trail => {
3725                    let op = self
3726                        .parse_flow_step_simple("trail")
3727                        .map(|l| FlowStep::Trail(TrailStep { navigate_ref: l.1, loc: l.0 }))?;
3728                    node.pix_ops.push(op);
3729                }
3730                // v2.83.0 — `validate <binding> against: <Schema>`, the
3731                // form README's pix family publishes inside a step. The
3732                // flow-level `validate <target>` already exists; this adds the
3733                // step position plus the `against:` clause the docs write.
3734                TokenType::Validate => {
3735                    let tok = self.current().clone();
3736                    self.advance();
3737                    // v2.83.0 — SUBJECT: `validate Assess.output against: X`.
3738                    let target = self.parse_subject()?;
3739                    let mut rule = String::new();
3740                    if self.current().value == "against" {
3741                        self.advance();
3742                        self.consume(TokenType::Colon)?;
3743                        rule = self.consume_any_ident_or_kw()?.value.clone();
3744                    }
3745                    node.pix_ops.push(FlowStep::Validate(ValidateStep {
3746                        target,
3747                        rule,
3748                        guard: None,
3749                        loc: Loc { line: tok.line, column: tok.column },
3750                    }));
3751                }
3752                // v2.88.0 — `if confidence < 0.8 -> refine(max_attempts: 2)`,
3753                // the self-correction guard blocks 1/16/18 publish immediately
3754                // after a `validate … against:`.
3755                //
3756                // Every position in the form is a CLOSED catalog of one — the
3757                // metric (`confidence`), the comparison (`<`), the action
3758                // (`refine`), the argument (`max_attempts`) — and each refusal
3759                // below names its catalog, because a free position here would
3760                // breed the imaginary catalog three cycles have now paid for.
3761                // General branching (`if <cond> { … } else { … }`) stays a
3762                // FLOW-level construct; a step body gets a guard or nothing.
3763                TokenType::If => {
3764                    let tok = self.current().clone();
3765                    self.advance();
3766
3767                    let metric = self.consume_any_ident_or_kw()?;
3768                    if metric.value != "confidence" {
3769                        return Err(ParseError {
3770                            message: format!(
3771                                "step-body `if` is the confidence guard — `if confidence < \
3772                                 <threshold> -> refine(max_attempts: <n>)` — and `confidence` \
3773                                 is its only metric (the CSR the preceding `validate … \
3774                                 against:` computes). Got '{}'. General branching belongs at \
3775                                 flow level: `if <cond> {{ … }}`.",
3776                                metric.value
3777                            ),
3778                            line: metric.line,
3779                            column: metric.column,
3780                            ..Default::default()
3781                        });
3782                    }
3783
3784                    let op = self.current().clone();
3785                    if op.ttype != TokenType::Lt {
3786                        return Err(ParseError {
3787                            message: format!(
3788                                "a confidence guard declares a FLOOR: `if confidence < \
3789                                 <threshold>`. `<` is the only comparison — the guard fires on \
3790                                 DEFICIENCY, and an inverted form would refine the outputs \
3791                                 that already conform. Got '{}'.",
3792                                op.value
3793                            ),
3794                            line: op.line,
3795                            column: op.column,
3796                            ..Default::default()
3797                        });
3798                    }
3799                    self.advance();
3800                    let threshold = self.consume_number()?;
3801
3802                    self.consume(TokenType::Arrow)?;
3803
3804                    if !self.check(TokenType::Refine) {
3805                        let bad = self.current().clone();
3806                        return Err(ParseError {
3807                            message: format!(
3808                                "the guard's action catalog is CLOSED and `refine` is its only \
3809                                 member — the recovery the runtime actually performs (re-derive \
3810                                 the validated value with the violations as feedback, then \
3811                                 re-score). Got '{}'. An action name outside the catalog would \
3812                                 advertise a recovery nothing dispatches.",
3813                                bad.value
3814                            ),
3815                            line: bad.line,
3816                            column: bad.column,
3817                            ..Default::default()
3818                        });
3819                    }
3820                    self.advance();
3821                    self.consume(TokenType::LParen)?;
3822                    let key = self.consume_any_ident_or_kw()?;
3823                    if key.value != "max_attempts" {
3824                        return Err(ParseError {
3825                            message: format!(
3826                                "`refine` takes exactly `max_attempts: <n>` — the bound that \
3827                                 makes the recovery loop TERMINATE by construction. Got '{}'.",
3828                                key.value
3829                            ),
3830                            line: key.line,
3831                            column: key.column,
3832                            ..Default::default()
3833                        });
3834                    }
3835                    self.consume(TokenType::Colon)?;
3836                    let attempts_tok = self.current().clone();
3837                    if attempts_tok.ttype != TokenType::Integer {
3838                        return Err(ParseError {
3839                            message: format!(
3840                                "`max_attempts:` must be a positive integer literal (got '{}')",
3841                                attempts_tok.value
3842                            ),
3843                            line: attempts_tok.line,
3844                            column: attempts_tok.column,
3845                            ..Default::default()
3846                        });
3847                    }
3848                    let max_attempts = attempts_tok.value.parse::<u32>().map_err(|_| ParseError {
3849                        message: format!("Invalid attempt count '{}'", attempts_tok.value),
3850                        line: attempts_tok.line,
3851                        column: attempts_tok.column,
3852                        ..Default::default()
3853                    })?;
3854                    self.advance();
3855                    self.consume(TokenType::RParen)?;
3856
3857                    // ATTACH to the validation this guard governs: the nearest
3858                    // preceding `validate … against:` in THIS step body. The
3859                    // attachment is what makes `confidence` unambiguous by
3860                    // construction — see `ast::ValidateStep::guard`. No such
3861                    // validation ⇒ the guard has nothing to read, and a guard
3862                    // over a score nobody computed is governance theatre.
3863                    let attached = node.pix_ops.iter_mut().rev().find_map(|op| match op {
3864                        FlowStep::Validate(v) if !v.rule.is_empty() => Some(v),
3865                        _ => None,
3866                    });
3867                    match attached {
3868                        Some(v) => {
3869                            if v.guard.is_some() {
3870                                return Err(ParseError {
3871                                    message: "this validation already carries a confidence \
3872                                              guard; a second one would race the first over \
3873                                              the same score. One validation, one floor, one \
3874                                              recovery."
3875                                        .to_string(),
3876                                    line: tok.line,
3877                                    column: tok.column,
3878                                    ..Default::default()
3879                                });
3880                            }
3881                            v.guard = Some(ConfidenceGuard {
3882                                threshold,
3883                                max_attempts,
3884                                loc: Loc { line: tok.line, column: tok.column },
3885                            });
3886                        }
3887                        None => {
3888                            return Err(ParseError {
3889                                message: "`if confidence` reads the CSR of a preceding \
3890                                          `validate … against: <Schema>` in this step body, \
3891                                          and none exists. A `validate` without `against:` \
3892                                          computes no score (there is no schema to score \
3893                                          with), so it cannot carry a guard either."
3894                                    .to_string(),
3895                                line: tok.line,
3896                                column: tok.column,
3897                                ..Default::default()
3898                            });
3899                        }
3900                    }
3901                }
3902                TokenType::Navigate => {
3903                    self.advance();
3904                    self.consume(TokenType::Colon)?;
3905                    node.navigate_ref = self.parse_dotted_identifier()?;
3906                }
3907                TokenType::Identifier if inner.value == "confidence_floor" => {
3908                    self.advance();
3909                    self.consume(TokenType::Colon)?;
3910                    node.confidence_floor = Some(self.consume_number()?);
3911                }
3912                TokenType::Identifier if inner.value == "apply" => {
3913                    self.advance();
3914                    self.consume(TokenType::Colon)?;
3915                    node.apply_ref = self.consume_any_ident_or_kw()?.value;
3916                }
3917                // v2.22.0 — `requires_context: <tokens>`: the step's declared
3918                // model-capability requirement (the context window the cognition
3919                // needs). A bare positive integer literal; the v2.22.0 resolver maps
3920                // it to a concrete model. Range/ceiling is the type-checker's job
3921                // (v2.22.0 positive-int + v2.22.0 catalog ceiling) — the parser only
3922                // requires an integer token here (a float / non-number is a parse
3923                // error, surfaced at the exact column).
3924                TokenType::Identifier if inner.value == "requires_context" => {
3925                    self.advance();
3926                    self.consume(TokenType::Colon)?;
3927                    let num = self.current().clone();
3928                    let bad = |tok: &crate::tokens::Token| ParseError {
3929                        message: format!(
3930                            "`requires_context:` must be a positive integer token count \
3931                             (got '{}')",
3932                            tok.value
3933                        ),
3934                        line: tok.line,
3935                        column: tok.column,
3936                        ..Default::default()
3937                    };
3938                    if num.ttype != TokenType::Integer {
3939                        return Err(bad(&num));
3940                    }
3941                    let value = num.value.parse::<u32>().map_err(|_| bad(&num))?;
3942                    self.advance();
3943                    node.requires_context = Some(value);
3944                }
3945                // v2.46.0 — `now: "<IANA-tz>"`: the step's declared cognitive
3946                // timezone. A string literal; the format law (IANA shape) is the
3947                // type-checker's job (`axon-T892`) — the parser only requires a
3948                // string token here, surfaced at the exact column.
3949                TokenType::Identifier if inner.value == "now" => {
3950                    self.advance();
3951                    self.consume(TokenType::Colon)?;
3952                    let tz = self.current().clone();
3953                    if tz.ttype != TokenType::StringLit {
3954                        return Err(ParseError {
3955                            message: format!(
3956                                "`now:` must be an IANA timezone string literal like \
3957                                 \"America/Bogota\" or \"UTC\" (got '{}')",
3958                                tz.value
3959                            ),
3960                            line: tz.line,
3961                            column: tz.column,
3962                            ..Default::default()
3963                        });
3964                    }
3965                    self.advance();
3966                    node.now_tz = Some(tz.value);
3967                }
3968                // v2.7.0 — a `use` nested inside a `step { }` body used
3969                // to be skipped structurally (grouped with the sub-constructs
3970                // below), silently degrading the tool dispatch to an
3971                // unconstrained LLM step with NO diagnostic. That fallthrough
3972                // drops the AST node before the type-checker can see it, so the
3973                // resource the tool would provision is never linearly accounted
3974                // for (use_tool soundness). Reject it here, at the parser —
3975                // the only place that still sees the token — and redirect to
3976                // the canonical forms.
3977                TokenType::Use => {
3978                    let tool = self
3979                        .tokens
3980                        .get(self.pos + 1)
3981                        .map(|t| t.value.as_str())
3982                        .filter(|v| !v.is_empty())
3983                        .unwrap_or("<Tool>");
3984                    return Err(ParseError {
3985                        message: format!(
3986                            "`use` is not valid inside a `step {{ }}` body — the tool dispatch \
3987                             would be silently dropped. To invoke a tool, either write the \
3988                             flow-level step `use {tool} on <arg>` (outside this block), or bind \
3989                             it inside this step with `apply: {tool}`. To attach a persona, put \
3990                             it in the step header: `step <name> use <Persona> {{ … }}`."
3991                        ),
3992                        line: inner.line,
3993                        column: inner.column,
3994                        ..Default::default()
3995                    });
3996                }
3997                // v2.83.0 — `mandate X on Y`, `shield X on Y -> b`,
3998                // `ots X on Y` as STEP-BODY statements. README XV has always
3999                // written the application here — next to the `output:` it
4000                // constrains — and the parser accepted the same form only at
4001                // flow level, which is why README blocks 40–42 never compiled.
4002                // The published position is also the better semantics: a
4003                // mandate inside a step is scoped to THIS step's generation;
4004                // the flow-level form governs a bare statement whose subject
4005                // must be inferred. One concept, two positions, same AST shape
4006                // as the flow-level `*ApplyStep` family.
4007                TokenType::Mandate => {
4008                    let g = self.parse_step_guard("mandate")?;
4009                    node.guards.push(g);
4010                }
4011                TokenType::Shield => {
4012                    let g = self.parse_step_guard("shield")?;
4013                    node.guards.push(g);
4014                }
4015                TokenType::Ots => {
4016                    let g = self.parse_step_guard("ots")?;
4017                    node.guards.push(g);
4018                }
4019                // v2.83.0 — `lambda RawQuote on ticker -> verified_quote`
4020                // inside a step body: README blocks 46-47's exact shape, the
4021                // the design decision statement position extended to the fourth member of
4022                // the apply family. Semantically it is an ELEVATION, not a
4023                // guard: dispatch runs it BEFORE the step's generation, so the
4024                // elevated binding is in scope for the prompt.
4025                TokenType::Lambda => {
4026                    let g = self.parse_step_guard("lambda")?;
4027                    node.guards.push(g);
4028                }
4029                // v2.83.0 — `probe <target> for [a, b, c]` as a STATEMENT.
4030                //
4031                // `probe` used to fall into `skip_flow_step_structural` below,
4032                // which DISCARDED it — the v2.67.0 silent-drop shape, in the step
4033                // parser. The extraction list had nowhere to live even at flow
4034                // level. Both are fixed here: the statement is kept, and its
4035                // `for [...]` list reaches the AST.
4036                TokenType::Probe
4037                    if self
4038                        .tokens
4039                        .get(self.pos + 1)
4040                        .is_some_and(|t| t.ttype != TokenType::Colon) =>
4041                {
4042                    let tok = self.current().clone();
4043                    self.advance();
4044                    // v2.83.0 — SUBJECT: README psyche writes
4045                    // `probe student.recent_interactions for [...]`.
4046                    let target = self.parse_subject()?;
4047                    let mut fields = Vec::new();
4048                    if self.check(TokenType::For) {
4049                        self.advance();
4050                        self.consume(TokenType::LBracket)?;
4051                        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
4052                            fields.push(self.consume_any_ident_or_kw()?.value.clone());
4053                            if self.check(TokenType::Comma) {
4054                                self.advance();
4055                            }
4056                        }
4057                        self.consume(TokenType::RBracket)?;
4058                    }
4059                    node.pix_ops.push(FlowStep::Probe(ProbeStep {
4060                        target,
4061                        fields,
4062                        loc: Loc { line: tok.line, column: tok.column },
4063                    }));
4064                }
4065                // v2.83.0 — `use_tool <name> [with k: v, …]` as a STATEMENT.
4066                // v2.7.0 made `use` inside a step body a hard error pointing at
4067                // the canonical forms; `use_tool` is the OTHER spelling README
4068                // publishes, and it names the tool explicitly, so there is no
4069                // ambiguity to protect against — the dispatch is not dropped,
4070                // it is recorded.
4071                TokenType::Identifier if inner.value == "use_tool" => {
4072                    let tok = self.current().clone();
4073                    self.advance();
4074                    let tool_name = self.consume_any_ident_or_kw()?.value.clone();
4075                    let args = if self.current().value == "with" {
4076                        self.advance();
4077                        let mut named: Vec<(String, String, String)> = Vec::new();
4078                        loop {
4079                            let k = self.consume_any_ident_or_kw()?.value.clone();
4080                            self.consume(TokenType::Colon)?;
4081                            // `value_kind` mirrors v2.10.0's classification: a
4082                            // string literal is a literal, anything else is a
4083                            // binding reference the runtime must look up.
4084                            let kind = if self.check(TokenType::StringLit) {
4085                                "literal"
4086                            } else {
4087                                "reference"
4088                            };
4089                            let v = self.parse_expression_string()?;
4090                            named.push((k, v, kind.to_string()));
4091                            if self.check(TokenType::Comma) {
4092                                self.advance();
4093                            } else {
4094                                break;
4095                            }
4096                        }
4097                        UseArgs::Named(named)
4098                    } else if self.current().value == "on" {
4099                        self.advance();
4100                        UseArgs::LegacyPositional(
4101                            self.consume_any_ident_or_kw()?.value.clone(),
4102                        )
4103                    } else {
4104                        UseArgs::LegacyPositional(String::new())
4105                    };
4106                    node.pix_ops.push(FlowStep::UseTool(UseToolStep {
4107                        tool_name,
4108                        args,
4109                        loc: Loc { line: tok.line, column: tok.column },
4110                    }));
4111                }
4112                // v2.83.0 — `par { … }` inside a step body.
4113                TokenType::Par => {
4114                    let block = self.parse_par_block()?;
4115                    node.pix_ops.push(FlowStep::Par(block));
4116                }
4117                // v2.83.0 — `reason { given: … ask: "…" depth: N }` as a
4118                // step-body statement. This is the README's single most-published
4119                // cognitive form (16 blocks) and it was the most expensive
4120                // resident of the silent-drop arm below: the block reached
4121                // `skip_flow_step_structural`, which discarded it, so a step
4122                // whose ONLY cognition was a `reason` lowered to an empty `ask`
4123                // and generated over nothing. The elevation position and the
4124                // flow position share `parse_reason_step` — one concept, two
4125                // positions.
4126                // v2.83.0 — `reason` in a step body is TWO forms, told
4127                // apart by the token after the keyword, exactly as v2.83.0 did
4128                // for `navigate`:
4129                //
4130                //   `reason: "…"`             the FIELD — a one-line deliberation
4131                //   `reason { given ask … }`  the STATEMENT README publishes
4132                //
4133                // The field form was already written across this repo's own
4134                // fixtures and it did NOTHING: `skip_flow_step_structural`
4135                // swallowed the key AND its value. Reading it as a `reason`
4136                // whose `ask:` is that value is not new semantics — it is the
4137                // block form with one field, which is what the line says.
4138                TokenType::Reason
4139                    if self
4140                        .tokens
4141                        .get(self.pos + 1)
4142                        .is_some_and(|t| t.ttype == TokenType::Colon) =>
4143                {
4144                    let tok = self.current().clone();
4145                    self.advance();
4146                    self.consume(TokenType::Colon)?;
4147                    let mut r = ReasonStep {
4148                        strategy: String::new(),
4149                        target: String::new(),
4150                        given: String::new(),
4151                        ask: String::new(),
4152                        depth: None,
4153                        loc: self.loc_of(&tok),
4154                    };
4155                    if self.check(TokenType::StringLit) {
4156                        r.ask = self.consume(TokenType::StringLit)?.value;
4157                    } else {
4158                        r.target = self.parse_dotted_identifier()?;
4159                    }
4160                    node.pix_ops.push(FlowStep::Reason(r));
4161                }
4162                TokenType::Reason => {
4163                    let r = self.parse_reason_step()?;
4164                    node.pix_ops.push(FlowStep::Reason(r));
4165                }
4166                // v2.83.0 — `weave [a, b] format: T include: […]` as a
4167                // step-body statement: the shape fourteen README blocks close
4168                // with. It was the worst resident of the silent-drop arm below,
4169                // because it did not merely lose the node — the skipper stops
4170                // at the first `output` KEYWORD it meets, so
4171                // `weave [A.output, B.output]` left the parser mid-list and the
4172                // step then failed with `Expected Colon` pointing at the comma.
4173                // A dropped construct AND a mislocated error.
4174                TokenType::Weave => {
4175                    let w = self.parse_weave_step()?;
4176                    node.pix_ops.push(w);
4177                }
4178                // v2.83.0 — `<Agent>(arg, …)` as a step-body statement:
4179                // the form every agent example in the README uses, and the one
4180                // that makes v2.83.0's executor reachable from source.
4181                //
4182                // Told apart from the field arms above by the `(` — those all
4183                // match on a specific field NAME, so a call can never shadow
4184                // one. The name is a NAME (never dotted: an agent declaration
4185                // has no path), the arguments are v2.83.0 SUBJECTS, because
4186                // README writes `TrendAnalyzer(Gather.output)`.
4187                TokenType::Identifier
4188                    if self
4189                        .tokens
4190                        .get(self.pos + 1)
4191                        .is_some_and(|t| t.ttype == TokenType::LParen) =>
4192                {
4193                    let tok = self.current().clone();
4194                    let agent_name = self.consume_any_ident_or_kw()?.value.clone();
4195                    self.consume(TokenType::LParen)?;
4196                    let mut arguments = Vec::new();
4197                    while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
4198                        arguments.push(self.parse_subject()?);
4199                        if self.check(TokenType::Comma) {
4200                            self.advance();
4201                        }
4202                    }
4203                    self.consume(TokenType::RParen)?;
4204                    node.pix_ops.push(FlowStep::AgentCall(AgentCallStep {
4205                        agent_name,
4206                        arguments,
4207                        loc: self.loc_of(&tok),
4208                    }));
4209                }
4210                // v2.83.0 — `retrieve from <Store> where "…"` as a
4211                // step-body statement. README axonstore writes the store read
4212                // INSIDE the step that consumes it, which is the elevation
4213                // position: the rows must be bound before the step generates.
4214                //
4215                // Unlike the three before it, this one needed no engine work —
4216                // `FlowStep::Retrieve` and `wire_integrations::run_retrieve`
4217                // are among the most-exercised paths in the system (v1.30.0–v1.31.0, the
4218                // pg integration suites). Only the position was missing.
4219                TokenType::Retrieve => {
4220                    let r = self.parse_retrieve_step()?;
4221                    node.pix_ops.push(r);
4222                }
4223                // v2.83.0 — `stream<T> { on_chunk: … on_complete: … }` in a
4224                // step body. THE LAST RESIDENT of the silent-drop arm leaves
4225                // here: `probe` left in v2.83.0, `reason` in v2.83.0, `weave` in
4226                // v2.83.0, `retrieve` in v2.83.0.
4227                //
4228                // What it cost, measured on README block 15 before this landed:
4229                // the whole block — a `probe`, a `validate`, and BOTH `output:`
4230                // declarations — went to `skip_flow_step_structural`, so
4231                // `step Stream` reached the dispatcher with `pix_ops=0`,
4232                // `ask=""`, `output=""`. An entirely EMPTY step, that `axon
4233                // check` passed with 0 errors, and whose `Stream.output` the
4234                // next step then reasoned over. The block had left the v2.81.0
4235                // ledger on the strength of compiling.
4236                //
4237                // NOT a `pix_ops` push — see `StepNode::stream`. The other ten
4238                // statements are elevations that run BEFORE generation; a stream
4239                // handler runs DURING it, and this step's output IS the stream.
4240                TokenType::Stream => {
4241                    let sb = self.parse_stream_block()?;
4242                    if node.stream.is_some() {
4243                        return Err(ParseError {
4244                            message:
4245                                "step declares two `stream` blocks; a step has one output stream, \
4246                                 and composing two has no defined meaning (which one is the \
4247                                 step's output?). Refused rather than silently keeping the last."
4248                                    .to_string(),
4249                            line: inner.line,
4250                            column: inner.column,
4251                            ..Default::default()
4252                        });
4253                    }
4254                    node.stream = Some(Box::new(sb));
4255                }
4256                // v2.87.0 — `perform Op(args)` in a step body, the position
4257                // `the design plan` section 3.1 publishes:
4258                //
4259                //     step generate {
4260                //         given: prompt
4261                //         perform Emit(response.token)
4262                //         perform Done()
4263                //     }
4264                //
4265                // NOT a `pix_ops` push, and this is the v2.83.0 lesson applied a
4266                // second time. Every `pix_ops` statement is an ELEVATION that
4267                // runs BEFORE the step generates. The performed ARGUMENT here is
4268                // the step's own output, so running it as an elevation would
4269                // hand the handler an unresolved symbol and put a NAME on the
4270                // wire where the adopter expected a token — a defect that shows
4271                // up as garbage output, never as an error.
4272                TokenType::Perform => {
4273                    let p = self.parse_perform_step()?;
4274                    node.performs.push(p);
4275                }
4276                // Sub-construct (probe, non-statement form) → skip structurally.
4277                // The REAL `probe … for […]` statement is taken by the guarded
4278                // arm above; this catches only the bare legacy shape.
4279                TokenType::Probe => {
4280                    self.skip_flow_step_structural()?;
4281                }
4282                _ => {
4283                    return Err(ParseError {
4284                        message: format!(
4285                            "Unexpected token in step body: '{}' — expected given, ask, \
4286                             probe, reason, weave, stream, perform, output, confidence_floor, \
4287                             navigate, apply, requires_context, now",
4288                            inner.value
4289                        ),
4290                        line: inner.line,
4291                        column: inner.column,
4292                                            ..Default::default()
4293                    });
4294                }
4295            }
4296        }
4297        Ok(())
4298    }
4299
4300    /// Skip a flow-level sub-construct structurally (consume keyword + args + optional block).
4301    fn skip_flow_step_structural(&mut self) -> Result<(), ParseError> {
4302        // Consume the keyword
4303        self.advance();
4304        // Consume tokens until we hit a { or a closing }, or a known flow step keyword
4305        while !self.check(TokenType::LBrace)
4306            && !self.check(TokenType::RBrace)
4307            && !self.check(TokenType::Eof)
4308        {
4309            // Check if we hit a new step-level keyword (means this was a one-liner)
4310            let tt = &self.current().ttype;
4311            if matches!(
4312                tt,
4313                TokenType::Step
4314                    | TokenType::Given
4315                    | TokenType::Ask
4316                    | TokenType::Output
4317                    | TokenType::Navigate
4318                    | TokenType::Use
4319                    | TokenType::Probe
4320                    | TokenType::Reason
4321                    | TokenType::Weave
4322                    | TokenType::Stream
4323                    | TokenType::If
4324                    | TokenType::For
4325                    | TokenType::Let
4326                    | TokenType::Return
4327            ) {
4328                return Ok(());
4329            }
4330            self.advance();
4331        }
4332        // If block, skip it
4333        if self.check(TokenType::LBrace) {
4334            self.skip_braced_block()?;
4335        }
4336        Ok(())
4337    }
4338
4339    // ── INTENT ───────────────────────────────────────────────────
4340
4341    fn parse_intent(&mut self) -> Result<IntentNode, ParseError> {
4342        let tok = self.consume(TokenType::Intent)?;
4343        let loc = self.loc_of(&tok);
4344        let name = self.consume(TokenType::Identifier)?.value;
4345        self.consume(TokenType::LBrace)?;
4346
4347        let mut node = IntentNode {
4348            name,
4349            given: String::new(),
4350            ask: String::new(),
4351            output_type: None,
4352            confidence_floor: None,
4353            loc,
4354            leading_trivia: Vec::new(),
4355            trailing_trivia: Vec::new(),
4356        };
4357
4358        while !self.check(TokenType::RBrace) {
4359            let field_name = self.current().value.clone();
4360            self.advance();
4361            self.consume(TokenType::Colon)?;
4362
4363            match field_name.as_str() {
4364                "given" => node.given = self.consume(TokenType::Identifier)?.value,
4365                "ask" => node.ask = self.consume(TokenType::StringLit)?.value,
4366                "output" => node.output_type = Some(self.parse_type_expr()?),
4367                "confidence_floor" => node.confidence_floor = Some(self.consume_number()?),
4368                _ => self.skip_value(),
4369            }
4370        }
4371        self.consume(TokenType::RBrace)?;
4372        Ok(node)
4373    }
4374
4375    // ── RUN ──────────────────────────────────────────────────────
4376
4377    fn parse_run(&mut self) -> Result<RunStatement, ParseError> {
4378        let tok = self.consume(TokenType::Run)?;
4379        let loc = self.loc_of(&tok);
4380        let flow_name = self.consume(TokenType::Identifier)?.value;
4381
4382        self.consume(TokenType::LParen)?;
4383        let mut arguments = Vec::new();
4384        if !self.check(TokenType::RParen) {
4385            arguments = self.parse_argument_list()?;
4386        }
4387        self.consume(TokenType::RParen)?;
4388
4389        let mut node = RunStatement {
4390            flow_name,
4391            arguments,
4392            persona: String::new(),
4393            context: String::new(),
4394            anchors: Vec::new(),
4395            on_failure: String::new(),
4396            on_failure_params: Vec::new(),
4397            output_to: String::new(),
4398            effort: String::new(),
4399            loc,
4400            leading_trivia: Vec::new(),
4401            trailing_trivia: Vec::new(),
4402        };
4403
4404        while self.check_run_modifier() {
4405            let mod_tok = self.current().clone();
4406            // v2.83.0 — `with <Persona>`, README's spelling of `as`.
4407            if mod_tok.value == "with" && mod_tok.ttype != TokenType::As {
4408                self.advance();
4409                node.persona = self.consume(TokenType::Identifier)?.value;
4410                continue;
4411            }
4412            match mod_tok.ttype {
4413                TokenType::As => {
4414                    self.advance();
4415                    node.persona = self.consume(TokenType::Identifier)?.value;
4416                }
4417                TokenType::Within => {
4418                    self.advance();
4419                    node.context = self.consume(TokenType::Identifier)?.value;
4420                }
4421                TokenType::ConstrainedBy => {
4422                    self.advance();
4423                    node.anchors = self.parse_bracketed_identifiers()?;
4424                }
4425                TokenType::OnFailure => {
4426                    self.advance();
4427                    self.consume(TokenType::Colon)?;
4428                    node.on_failure = self.consume_any_ident_or_kw()?.value;
4429                    // Parse optional params: (key: val, ...)
4430                    if self.check(TokenType::LParen) {
4431                        self.advance();
4432                        while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
4433                            let key = self.consume_any_ident_or_kw()?.value;
4434                            self.consume(TokenType::Colon)?;
4435                            let val = self.consume_any_ident_or_kw()?.value;
4436                            node.on_failure_params.push((key, val));
4437                            if self.check(TokenType::Comma) {
4438                                self.advance();
4439                            }
4440                        }
4441                        if self.check(TokenType::RParen) {
4442                            self.advance();
4443                        }
4444                    }
4445                }
4446                TokenType::OutputTo => {
4447                    self.advance();
4448                    self.consume(TokenType::Colon)?;
4449                    node.output_to = self.consume(TokenType::StringLit)?.value;
4450                }
4451                TokenType::Effort => {
4452                    self.advance();
4453                    self.consume(TokenType::Colon)?;
4454                    node.effort = self.consume_any_ident_or_kw()?.value;
4455                }
4456                _ => break,
4457            }
4458        }
4459
4460        Ok(node)
4461    }
4462
4463    // ── EPISTEMIC BLOCK ──────────────────────────────────────────
4464
4465    fn parse_epistemic_block(&mut self) -> Result<EpistemicBlock, ParseError> {
4466        let tok = self.current().clone();
4467        let mode = match tok.ttype {
4468            TokenType::Know => "know",
4469            TokenType::Believe => "believe",
4470            TokenType::Speculate => "speculate",
4471            TokenType::Doubt => "doubt",
4472            _ => unreachable!(),
4473        };
4474        self.advance();
4475        let loc = self.loc_of(&tok);
4476
4477        self.consume(TokenType::LBrace)?;
4478        let mut body = Vec::new();
4479        while !self.check(TokenType::RBrace) {
4480            body.push(self.parse_declaration()?);
4481        }
4482        self.consume(TokenType::RBrace)?;
4483
4484        Ok(EpistemicBlock {
4485            mode: mode.to_string(),
4486            body,
4487            loc,
4488            leading_trivia: Vec::new(),
4489            trailing_trivia: Vec::new(),
4490        })
4491    }
4492
4493    // ── IF ────────────────────────────────────────────────────────
4494
4495    // ── v2.26.0 — the pure expression engine (Pratt parser) ───────────
4496
4497    /// Parse a pure expression (v2.26.0). Precedence-climbing: `or` < `and` <
4498    /// comparison < `+ -` < `* / %` < unary (`- not`) < atom. Total + pure; no
4499    /// side effects. Field/index access + the builtin catalog land in v2.26.0.
4500    fn parse_expr(&mut self) -> Result<Expr, ParseError> {
4501        self.parse_expr_bp(0)
4502    }
4503
4504    fn parse_expr_bp(&mut self, min_bp: u8) -> Result<Expr, ParseError> {
4505        // Prefix: unary `-` (negation) / `not` (boolean). Binds tighter than
4506        // every binary operator (bp 6).
4507        let mut lhs = match self.current().ttype {
4508            TokenType::Minus => {
4509                self.advance();
4510                Expr::Unary(UnOp::Neg, Box::new(self.parse_expr_bp(6)?))
4511            }
4512            TokenType::Not => {
4513                self.advance();
4514                Expr::Unary(UnOp::Not, Box::new(self.parse_expr_bp(6)?))
4515            }
4516            _ => self.parse_postfix()?,
4517        };
4518        // Infix: left-associative (right_bp = left_bp + 1).
4519        while let Some((op, lbp)) = Self::binop_of(self.current().ttype.clone()) {
4520            if lbp < min_bp {
4521                break;
4522            }
4523            self.advance();
4524            let rhs = self.parse_expr_bp(lbp + 1)?;
4525            lhs = Expr::Binary(op, Box::new(lhs), Box::new(rhs));
4526        }
4527        Ok(lhs)
4528    }
4529
4530    /// Map a token to `(BinOp, left binding power)`, or `None` if it is not an
4531    /// infix operator (which stops the climb — e.g. at `->` or `{`).
4532    fn binop_of(t: TokenType) -> Option<(BinOp, u8)> {
4533        Some(match t {
4534            TokenType::Or => (BinOp::Or, 1),
4535            TokenType::And => (BinOp::And, 2),
4536            TokenType::Eq => (BinOp::Eq, 3),
4537            TokenType::Neq => (BinOp::Ne, 3),
4538            TokenType::Lt => (BinOp::Lt, 3),
4539            TokenType::Lte => (BinOp::Le, 3),
4540            TokenType::Gt => (BinOp::Gt, 3),
4541            TokenType::Gte => (BinOp::Ge, 3),
4542            TokenType::Plus => (BinOp::Add, 4),
4543            TokenType::Minus => (BinOp::Sub, 4),
4544            TokenType::Star => (BinOp::Mul, 5),
4545            TokenType::Slash => (BinOp::Div, 5),
4546            TokenType::Percent => (BinOp::Mod, 5),
4547            _ => return None,
4548        })
4549    }
4550
4551    /// v2.26.0 — parse a primary then its `.` postfix chain: a builtin call
4552    /// (`.length`, `.contains(x)`) when the name is in the closed catalog, else
4553    /// a dotted reference-path continuation (`a.b.c` → `Ref("a.b.c")`, the
4554    /// pre-v2.26.0 behaviour). Field access on a non-reference (`(a+b).x`) is
4555    /// reserved for v2.26.0.
4556    fn parse_postfix(&mut self) -> Result<Expr, ParseError> {
4557        let mut expr = self.parse_expr_atom()?;
4558        loop {
4559            if self.check(TokenType::Dot) {
4560                self.advance();
4561                let name = self.consume_any_ident_or_kw()?.value;
4562                if let Some(builtin) = Builtin::from_name(&name) {
4563                    let mut args = vec![expr];
4564                    if self.check(TokenType::LParen) {
4565                        self.advance();
4566                        while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
4567                            args.push(self.parse_expr_bp(0)?);
4568                            if self.check(TokenType::Comma) {
4569                                self.advance();
4570                            } else {
4571                                break;
4572                            }
4573                        }
4574                        self.consume(TokenType::RParen)?;
4575                    }
4576                    expr = Expr::Call(builtin, args);
4577                } else {
4578                    // v2.26.0 — a plain dotted path on a Ref extends the Ref
4579                    // (back-compat: `a.b.c` → `Ref("a.b.c")`); on any other base
4580                    // it is a structured field access (the JSONB seam).
4581                    expr = match expr {
4582                        Expr::Ref(p) => Expr::Ref(format!("{p}.{name}")),
4583                        other => Expr::Field(Box::new(other), name),
4584                    };
4585                }
4586            } else if self.check(TokenType::LBracket) {
4587                // v2.26.0 — index access `base[index]`.
4588                self.advance();
4589                let index = self.parse_expr_bp(0)?;
4590                self.consume(TokenType::RBracket)?;
4591                expr = Expr::Index(Box::new(expr), Box::new(index));
4592            } else {
4593                break;
4594            }
4595        }
4596        Ok(expr)
4597    }
4598
4599    fn parse_expr_atom(&mut self) -> Result<Expr, ParseError> {
4600        let tok = self.current().clone();
4601        match tok.ttype {
4602            TokenType::Integer => {
4603                self.advance();
4604                let lit = tok
4605                    .value
4606                    .parse::<i64>()
4607                    .map(ExprLit::Int)
4608                    .or_else(|_| tok.value.parse::<f64>().map(ExprLit::Float))
4609                    .map_err(|_| ParseError {
4610                        message: format!("invalid integer literal '{}'", tok.value),
4611                        line: tok.line,
4612                        column: tok.column,
4613                        ..Default::default()
4614                    })?;
4615                Ok(Expr::Lit(lit))
4616            }
4617            TokenType::Float => {
4618                self.advance();
4619                let f = tok.value.parse::<f64>().map_err(|_| ParseError {
4620                    message: format!("invalid float literal '{}'", tok.value),
4621                    line: tok.line,
4622                    column: tok.column,
4623                    ..Default::default()
4624                })?;
4625                Ok(Expr::Lit(ExprLit::Float(f)))
4626            }
4627            TokenType::Bool => {
4628                self.advance();
4629                Ok(Expr::Lit(ExprLit::Bool(tok.value == "true")))
4630            }
4631            TokenType::StringLit => {
4632                self.advance();
4633                Ok(Expr::Lit(ExprLit::Str(tok.value)))
4634            }
4635            TokenType::LParen => {
4636                self.advance();
4637                let inner = self.parse_expr_bp(0)?;
4638                self.consume(TokenType::RParen)?;
4639                Ok(inner)
4640            }
4641            _ => {
4642                // Reference: a single identifier (or keyword used as a name).
4643                // The `.` chain (dotted path / builtin call) is handled by the
4644                // postfix layer (v2.26.0 `parse_postfix`).
4645                Ok(Expr::Ref(self.consume_any_ident_or_kw()?.value))
4646            }
4647        }
4648    }
4649
4650    /// v2.26.0 — render a literal to its legacy surface string (for the
4651    /// back-compat `(condition, op, value)` triple). Only used when an
4652    /// expression fits the legacy shape; numeric round-tripping is exact for
4653    /// ints and faithful-enough for floats (the legacy runtime re-parses it).
4654    fn expr_lit_surface(lit: &ExprLit) -> String {
4655        match lit {
4656            ExprLit::Int(i) => i.to_string(),
4657            ExprLit::Float(f) => f.to_string(),
4658            ExprLit::Bool(b) => b.to_string(),
4659            ExprLit::Str(s) => s.clone(),
4660        }
4661    }
4662
4663    fn expr_leaf_surface(expr: &Expr) -> Option<String> {
4664        match expr {
4665            Expr::Ref(p) => Some(p.clone()),
4666            Expr::Lit(l) => Some(Self::expr_lit_surface(l)),
4667            _ => None,
4668        }
4669    }
4670
4671    /// A legacy "leaf" is a bare reference (truthy check) or a
4672    /// `<ref> <cmp> <ref|literal>` triple — exactly what the pre-v2.26.0 `if`
4673    /// grammar could express.
4674    fn expr_legacy_leaf(expr: &Expr) -> Option<(String, String, String)> {
4675        match expr {
4676            Expr::Ref(p) => Some((p.clone(), String::new(), String::new())),
4677            Expr::Binary(op, l, r) => {
4678                let op_s = match op {
4679                    BinOp::Eq => "==",
4680                    BinOp::Ne => "!=",
4681                    BinOp::Lt => "<",
4682                    BinOp::Le => "<=",
4683                    BinOp::Gt => ">",
4684                    BinOp::Ge => ">=",
4685                    _ => return None,
4686                };
4687                let lhs = match &**l {
4688                    Expr::Ref(p) => p.clone(),
4689                    _ => return None,
4690                };
4691                let rhs = Self::expr_leaf_surface(r)?;
4692                Some((lhs, op_s.to_string(), rhs))
4693            }
4694            _ => None,
4695        }
4696    }
4697
4698    /// Flatten an `or`-tree of legacy leaves in left-to-right order. Returns
4699    /// `false` (and leaves `out` unusable) if any node is not a legacy leaf.
4700    fn collect_or_leaves(expr: &Expr, out: &mut Vec<(String, String, String)>) -> bool {
4701        match expr {
4702            Expr::Binary(BinOp::Or, l, r) => {
4703                Self::collect_or_leaves(l, out) && Self::collect_or_leaves(r, out)
4704            }
4705            _ => match Self::expr_legacy_leaf(expr) {
4706                Some(t) => {
4707                    out.push(t);
4708                    true
4709                }
4710                None => false,
4711            },
4712        }
4713    }
4714
4715    /// v2.26.0 — if the parsed condition fits the legacy
4716    /// `(condition, op, value)` + `or`-chain shape, return the legacy fields so
4717    /// the IR + runtime stay byte-identical to pre-v2.26.0 (zero drift). `None` ⇒
4718    /// the condition uses richer forms (`and`, `not`, arithmetic, parentheses,
4719    /// nesting) and must ride the `cond` expression evaluator.
4720    #[allow(clippy::type_complexity)]
4721    fn cond_as_legacy(
4722        expr: &Expr,
4723    ) -> Option<(String, String, String, Vec<(String, String, String)>, String)> {
4724        let mut leaves = Vec::new();
4725        if !Self::collect_or_leaves(expr, &mut leaves) || leaves.is_empty() {
4726            return None;
4727        }
4728        let (c0, o0, v0) = leaves[0].clone();
4729        let rest = leaves[1..].to_vec();
4730        let conjunctor = if rest.is_empty() {
4731            String::new()
4732        } else {
4733            "or".to_string()
4734        };
4735        Some((c0, o0, v0, rest, conjunctor))
4736    }
4737
4738    fn parse_if(&mut self) -> Result<ConditionalNode, ParseError> {
4739        let tok = self.consume(TokenType::If)?;
4740        let loc = self.loc_of(&tok);
4741
4742        // v2.26.0 — parse the condition as a pure expression, then split:
4743        // a legacy-expressible condition populates the legacy triple fields
4744        // (cond = None → byte-identical IR + eval); a richer condition rides
4745        // the `cond` expression evaluator.
4746        let expr = self.parse_expr()?;
4747        let (condition, comparison_op, comparison_value, conditions, conjunctor, cond) =
4748            match Self::cond_as_legacy(&expr) {
4749                Some((c, o, v, more, conj)) => (c, o, v, more, conj, None),
4750                None => (
4751                    String::new(),
4752                    String::new(),
4753                    String::new(),
4754                    Vec::new(),
4755                    String::new(),
4756                    Some(expr),
4757                ),
4758            };
4759
4760        let mut then_body = Vec::new();
4761        let mut else_body = Vec::new();
4762
4763        // Arrow form or block form
4764        if self.check(TokenType::Arrow) {
4765            self.advance();
4766            then_body.push(self.parse_flow_step()?);
4767        } else if self.check(TokenType::LBrace) {
4768            self.advance();
4769            while !self.check(TokenType::RBrace) {
4770                then_body.push(self.parse_flow_step()?);
4771            }
4772            self.consume(TokenType::RBrace)?;
4773        }
4774
4775        // Else branch
4776        if self.check(TokenType::Else) {
4777            self.advance();
4778            if self.check(TokenType::Arrow) {
4779                self.advance();
4780                else_body.push(self.parse_flow_step()?);
4781            } else if self.check(TokenType::LBrace) {
4782                self.advance();
4783                while !self.check(TokenType::RBrace) {
4784                    else_body.push(self.parse_flow_step()?);
4785                }
4786                self.consume(TokenType::RBrace)?;
4787            }
4788        }
4789
4790        Ok(ConditionalNode {
4791            condition,
4792            comparison_op,
4793            comparison_value,
4794            then_body,
4795            else_body,
4796            conditions,
4797            conjunctor,
4798            cond,
4799            loc,
4800        })
4801    }
4802
4803    // ── FOR IN ───────────────────────────────────────────────────
4804
4805    fn parse_for_in(&mut self) -> Result<ForInStatement, ParseError> {
4806        let tok = self.consume(TokenType::For)?;
4807        let loc = self.loc_of(&tok);
4808        let variable = self.consume(TokenType::Identifier)?.value;
4809        self.consume(TokenType::In)?;
4810        let iterable = self.parse_dotted_identifier()?;
4811
4812        self.consume(TokenType::LBrace)?;
4813        // v1.14.0 — increment loop_depth so `parse_break` /
4814        // `parse_continue` inside the body pass the scope check.
4815        // Decrement on every exit path (Ok / Err) so a parse error
4816        // mid-body does not leave the depth permanently elevated
4817        // for later top-level parsing — `?` would skip the
4818        // decrement otherwise.
4819        self.loop_depth += 1;
4820        let body_result = (|| -> Result<Vec<FlowStep>, ParseError> {
4821            let mut body = Vec::new();
4822            while !self.check(TokenType::RBrace) {
4823                body.push(self.parse_flow_step()?);
4824            }
4825            Ok(body)
4826        })();
4827        self.loop_depth -= 1;
4828        let body = body_result?;
4829        self.consume(TokenType::RBrace)?;
4830
4831        Ok(ForInStatement {
4832            variable,
4833            iterable,
4834            body,
4835            loc,
4836        })
4837    }
4838
4839    /// v1.14.0 — `break` keyword. Compile-time scope check
4840    /// (`loop_depth == 0`) rejects break outside a for-in body.
4841    fn parse_break(&mut self) -> Result<BreakStatement, ParseError> {
4842        let tok = self.consume(TokenType::Break)?;
4843        let loc = self.loc_of(&tok);
4844        if self.loop_depth == 0 {
4845            return Err(ParseError {
4846                message: "'break' outside of a for-in loop body".to_string(),
4847                line: tok.line,
4848                column: tok.column,
4849                            ..Default::default()
4850            });
4851        }
4852        Ok(BreakStatement { loc })
4853    }
4854
4855    /// v1.14.0 — `continue` keyword. Same scope check as
4856    /// `parse_break`.
4857    fn parse_continue(&mut self) -> Result<ContinueStatement, ParseError> {
4858        let tok = self.consume(TokenType::Continue)?;
4859        let loc = self.loc_of(&tok);
4860        if self.loop_depth == 0 {
4861            return Err(ParseError {
4862                message: "'continue' outside of a for-in loop body".to_string(),
4863                line: tok.line,
4864                column: tok.column,
4865                            ..Default::default()
4866            });
4867        }
4868        Ok(ContinueStatement { loc })
4869    }
4870
4871    // ── LET ──────────────────────────────────────────────────────
4872
4873    fn parse_let(&mut self) -> Result<LetStatement, ParseError> {
4874        let tok = self.consume(TokenType::Let)?;
4875        let loc = self.loc_of(&tok);
4876
4877        // Name can be an identifier or a keyword used as binding name
4878        let name = self.consume_any_ident_or_kw()?.value;
4879        // v2.4.0 — optional type annotation `let x: <TypeExpr> = …`.
4880        let type_annotation = if self.check(TokenType::Colon) {
4881            self.advance();
4882            Some(self.parse_type_expr()?)
4883        } else {
4884            None
4885        };
4886        self.consume(TokenType::Assign)?;
4887        // v1.12.0 — reset side-channel before parsing value; the
4888        // atom / expr helpers tag the kind as they descend.
4889        self.last_let_value_kind = "literal".to_string();
4890        let (value, value_ast) = self.parse_let_value_expr_with_ast()?;
4891
4892        Ok(LetStatement {
4893            identifier: name,
4894            value_expr: value,
4895            value_kind: self.last_let_value_kind.clone(),
4896            type_annotation,
4897            value_ast,
4898            loc,
4899            leading_trivia: Vec::new(),
4900            trailing_trivia: Vec::new(),
4901        })
4902    }
4903
4904    fn parse_let_value_expr(&mut self) -> Result<String, ParseError> {
4905        let atom = self.parse_let_atom()?;
4906
4907        // Arithmetic expression: collect as string
4908        if matches!(
4909            self.current().ttype,
4910            TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
4911        ) {
4912            let mut parts = vec![atom];
4913            while matches!(
4914                self.current().ttype,
4915                TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
4916            ) {
4917                parts.push(self.advance().value.clone());
4918                parts.push(self.parse_let_atom()?);
4919            }
4920            self.last_let_value_kind = "expression".to_string();
4921            return Ok(parts.join(" "));
4922        }
4923        Ok(atom)
4924    }
4925
4926    /// v2.26.0 — parse a `let`-binding value, additionally producing a
4927    /// structured `value_ast` for the expression case. A list literal keeps the
4928    /// dedicated path; everything else is parsed through the v2.26.0 expression
4929    /// engine and classified: a bare literal / reference keeps its pre-v2.26.0
4930    /// string form (`value_ast = None`, byte-identical), while a real expression
4931    /// (`price * qty`, `recent.length`) additionally carries a `value_ast` the
4932    /// runtime evaluates for real (pre-v2.26.0 it was treated as an opaque literal
4933    /// string). Used ONLY by `parse_let` — other value positions (list items,
4934    /// remember/stream values) keep the string-only `parse_let_value_expr`.
4935    fn parse_let_value_expr_with_ast(&mut self) -> Result<(String, Option<Expr>), ParseError> {
4936        if self.check(TokenType::LBracket) {
4937            self.last_let_value_kind = "literal".to_string();
4938            return Ok((self.parse_let_list_literal()?, None));
4939        }
4940        let expr = self.parse_expr()?;
4941        Ok(match expr {
4942            Expr::Lit(lit) => {
4943                self.last_let_value_kind = "literal".to_string();
4944                (Self::expr_lit_surface(&lit), None)
4945            }
4946            Expr::Ref(p) => {
4947                self.last_let_value_kind = "reference".to_string();
4948                (p, None)
4949            }
4950            other => {
4951                self.last_let_value_kind = "expression".to_string();
4952                (Self::render_expr(&other), Some(other))
4953            }
4954        })
4955    }
4956
4957    /// v2.26.0 — a readable surface rendering of an expression for the
4958    /// vestigial `value_expr` string (the runtime uses `value_ast`).
4959    fn render_expr(e: &Expr) -> String {
4960        match e {
4961            Expr::Lit(l) => Self::expr_lit_surface(l),
4962            Expr::Ref(p) => p.clone(),
4963            // v2.83.0 — surface form of a `logic { }` chain. This string is
4964            // vestigial (the runtime evaluates `value_ast`), so it renders the
4965            // shape rather than trying to reconstruct the author's layout.
4966            Expr::Let { name, value, body } => format!(
4967                "let {name} = {} in {}",
4968                Self::render_expr(value),
4969                Self::render_expr(body)
4970            ),
4971            Expr::Unary(UnOp::Neg, x) => format!("-{}", Self::render_expr(x)),
4972            Expr::Unary(UnOp::Not, x) => format!("not {}", Self::render_expr(x)),
4973            Expr::Binary(op, l, r) => {
4974                let sym = match op {
4975                    BinOp::Add => "+",
4976                    BinOp::Sub => "-",
4977                    BinOp::Mul => "*",
4978                    BinOp::Div => "/",
4979                    BinOp::Mod => "%",
4980                    BinOp::Eq => "==",
4981                    BinOp::Ne => "!=",
4982                    BinOp::Lt => "<",
4983                    BinOp::Le => "<=",
4984                    BinOp::Gt => ">",
4985                    BinOp::Ge => ">=",
4986                    BinOp::And => "and",
4987                    BinOp::Or => "or",
4988                };
4989                format!("({} {sym} {})", Self::render_expr(l), Self::render_expr(r))
4990            }
4991            Expr::Call(b, args) => {
4992                let recv = args.first().map(Self::render_expr).unwrap_or_default();
4993                let rest: Vec<String> = args.iter().skip(1).map(Self::render_expr).collect();
4994                if rest.is_empty() {
4995                    format!("{recv}.{}", b.surface())
4996                } else {
4997                    format!("{recv}.{}({})", b.surface(), rest.join(", "))
4998                }
4999            }
5000            Expr::Field(b, f) => format!("{}.{f}", Self::render_expr(b)),
5001            Expr::Index(b, i) => format!("{}[{}]", Self::render_expr(b), Self::render_expr(i)),
5002        }
5003    }
5004
5005    fn parse_let_atom(&mut self) -> Result<String, ParseError> {
5006        let tok = self.current().clone();
5007
5008        match tok.ttype {
5009            TokenType::StringLit => {
5010                self.last_let_value_kind = "literal".to_string();
5011                self.advance();
5012                Ok(tok.value)
5013            }
5014            TokenType::Integer | TokenType::Float => {
5015                self.last_let_value_kind = "literal".to_string();
5016                self.advance();
5017                Ok(tok.value)
5018            }
5019            TokenType::Bool => {
5020                self.last_let_value_kind = "literal".to_string();
5021                self.advance();
5022                Ok(tok.value)
5023            }
5024            TokenType::Identifier => {
5025                self.last_let_value_kind = "reference".to_string();
5026                self.parse_dotted_identifier()
5027            }
5028            TokenType::LBracket => {
5029                self.last_let_value_kind = "literal".to_string();
5030                self.parse_let_list_literal()
5031            }
5032            _ => {
5033                // Keywords starting a dotted path (pix.document_tree)
5034                if self.pos + 1 < self.tokens.len()
5035                    && self.tokens[self.pos + 1].ttype == TokenType::Dot
5036                {
5037                    self.last_let_value_kind = "reference".to_string();
5038                    return self.parse_dotted_identifier();
5039                }
5040                Err(ParseError {
5041                    message: format!(
5042                        "Expected value expression, found {:?}('{}')",
5043                        tok.ttype, tok.value
5044                    ),
5045                    line: tok.line,
5046                    column: tok.column,
5047                                    ..Default::default()
5048                })
5049            }
5050        }
5051    }
5052
5053    fn parse_let_list_literal(&mut self) -> Result<String, ParseError> {
5054        self.consume(TokenType::LBracket)?;
5055        let mut items = Vec::new();
5056        if !self.check(TokenType::RBracket) {
5057            items.push(self.parse_let_value_expr()?);
5058            while self.check(TokenType::Comma) {
5059                self.advance();
5060                if self.check(TokenType::RBracket) {
5061                    break; // trailing comma
5062                }
5063                items.push(self.parse_let_value_expr()?);
5064            }
5065        }
5066        self.consume(TokenType::RBracket)?;
5067        Ok(format!("[{}]", items.join(", ")))
5068    }
5069
5070    // ── RETURN ───────────────────────────────────────────────────
5071
5072    fn parse_return(&mut self) -> Result<ReturnStatement, ParseError> {
5073        let tok = self.consume(TokenType::Return)?;
5074        let loc = self.loc_of(&tok);
5075        let value = self.parse_let_value_expr()?;
5076        Ok(ReturnStatement {
5077            value_expr: value,
5078            loc,
5079        })
5080    }
5081
5082    // ── TIER 2 FLOW STEP HELPERS ────────────────────────────────────
5083
5084    /// Parse: keyword target (consumes keyword + one identifier/keyword-as-value).
5085    fn parse_flow_step_simple(&mut self, _kw: &str) -> Result<(Loc, String), ParseError> {
5086        let tok = self.current().clone();
5087        self.advance(); // consume keyword
5088        let target = if self.at_declaration_start()
5089            || self.check(TokenType::RBrace)
5090            || self.check(TokenType::Eof)
5091        {
5092            String::new()
5093        } else {
5094            self.consume_any_ident_or_kw()?.value.clone()
5095        };
5096        // Skip optional braced block
5097        if self.check(TokenType::LBrace) {
5098            self.skip_braced_block()?;
5099        }
5100        Ok((
5101            Loc {
5102                line: tok.line,
5103                column: tok.column,
5104            },
5105            target,
5106        ))
5107    }
5108
5109    /// Parse: keyword { ... } — block-level step, skip body structurally.
5110    /// v2.67.0 — `stream { <steps> }` with a REAL body.
5111    ///
5112    /// The four block primitives (`deliberate`, `consensus`, `stream`,
5113    /// `transact`) all went through [`Self::parse_block_step`], whose entire job
5114    /// is `skip_braced_block()`. Their bodies were discarded at parse time — so
5115    /// their handlers were not no-ops through neglect, they were no-ops
5116    /// *by construction*: there was nothing in the AST to execute. v2.67.0 retracted
5117    /// `transact`; this gives `stream` its body back. `deliberate` / `consensus`
5118    /// remain body-less pending their Tier-4 disposition.
5119    fn parse_stream_block(&mut self) -> Result<StreamBlock, ParseError> {
5120        let tok = self.current().clone();
5121        let loc = self.loc_of(&tok);
5122        self.advance(); // consume `stream`
5123
5124        // v2.83.0 — `<T>`: the CHUNK type, and the reason this is not just a
5125        // cosmetic capture. The skip loop below used to eat it: `stream<QuoteData>`
5126        // advanced straight past `<QuoteData>` looking for `{`, so the one piece of
5127        // type information the author wrote about the stream was discarded before
5128        // anything could check it.
5129        let mut chunk_type = String::new();
5130        if self.check(TokenType::Lt) {
5131            self.advance();
5132            let inner = self.parse_type_expr()?;
5133            chunk_type = if inner.generic_param.is_empty() {
5134                inner.name
5135            } else {
5136                format!("{}<{}>", inner.name, inner.generic_param)
5137            };
5138            self.consume(TokenType::Gt)?;
5139        }
5140
5141        // Tolerate the pre-111 form `stream <effect-ish tokens> { … }`: skip any
5142        // argument tokens ahead of the brace, exactly as `parse_block_step` did,
5143        // so an existing program keeps parsing. Only the BODY changes.
5144        while !self.check(TokenType::LBrace)
5145            && !self.check(TokenType::RBrace)
5146            && !self.check(TokenType::Eof)
5147            && !self.at_declaration_start()
5148        {
5149            self.advance();
5150        }
5151
5152        let mut block = StreamBlock {
5153            chunk_type,
5154            on_chunk: None,
5155            on_complete: None,
5156            on_error: None,
5157            body: Vec::new(),
5158            loc,
5159        };
5160
5161        if self.check(TokenType::LBrace) {
5162            self.advance();
5163            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5164                // v2.83.0 — the two SPECIFIED handler arms. `the design plan`'s D8
5165                // promises `stream<τ> { on_chunk: … on_complete: … }` compiles
5166                // with "cero cambios en `.axon` source files de adopters"; before
5167                // this landed it was a hard parse error at flow level and a
5168                // silent discard in a step body.
5169                let name = self.current().value.clone();
5170                let is_arm = matches!(name.as_str(), "on_chunk" | "on_complete" | "on_error")
5171                    && self
5172                        .tokens
5173                        .get(self.pos + 1)
5174                        .is_some_and(|t| t.ttype == TokenType::Colon);
5175                if is_arm {
5176                    let arm_tok = self.current().clone();
5177                    self.advance(); // the handler name
5178                    self.advance(); // `:`
5179                    let arm = self.parse_stream_handler_arm(&name, &arm_tok)?;
5180                    let slot = match name.as_str() {
5181                        "on_chunk" => &mut block.on_chunk,
5182                        "on_complete" => &mut block.on_complete,
5183                        _ => &mut block.on_error,
5184                    };
5185                    if slot.is_some() {
5186                        return Err(ParseError {
5187                            message: format!(
5188                                "`{name}` is declared twice in this `stream` block. Two handlers \
5189                                 for one edge have no defined composition (whose output is the \
5190                                 stream's?), so the duplicate is refused rather than silently \
5191                                 overwriting the first."
5192                            ),
5193                            line: arm_tok.line,
5194                            column: arm_tok.column,
5195                            ..Default::default()
5196                        });
5197                    }
5198                    *slot = Some(arm);
5199                    continue;
5200                }
5201
5202                // A `<ident>: {` that is NOT one of the two arms is a TYPO in a
5203                // closed catalog, and the v2.83.0 discipline says to ask which
5204                // direction the silence fails in: a mis-spelled `on_chunk` would
5205                // fall through to `parse_flow_step` and be reported against the
5206                // brace, pointing the author at the wrong token entirely. Name
5207                // the key and the catalog instead.
5208                let next_two_are_block = self
5209                    .tokens
5210                    .get(self.pos + 1)
5211                    .is_some_and(|t| t.ttype == TokenType::Colon)
5212                    && self
5213                        .tokens
5214                        .get(self.pos + 2)
5215                        .is_some_and(|t| t.ttype == TokenType::LBrace);
5216                if next_two_are_block {
5217                    let bad = self.current().clone();
5218                    return Err(ParseError {
5219                        message: format!(
5220                            "unknown `stream` handler `{name}` — this block accepts only \
5221                             `on_chunk:` (run once per chunk, with the chunk bound as `chunk`), \
5222                             `on_complete:` (run once, after the source closes, with the \
5223                             accumulation bound as `complete`) and `on_error:` (run when the \
5224                             SOURCE fails, with the failure bound as `error`). An unrecognised \
5225                             handler is refused rather than skipped: a skipped handler removes \
5226                             the processing the author wrote, and silence in that direction is \
5227                             indistinguishable from a stream that had nothing to do."
5228                        ),
5229                        line: bad.line,
5230                        column: bad.column,
5231                        ..Default::default()
5232                    });
5233                }
5234
5235                // v2.67.0's body form, kept: `stream { <flow steps> }`.
5236                block.body.push(self.parse_flow_step()?);
5237            }
5238            self.consume(TokenType::RBrace)?;
5239        }
5240
5241        Ok(block)
5242    }
5243
5244    /// v2.83.0 — one `on_chunk:` / `on_complete:` arm, parsed as a STEP body.
5245    ///
5246    /// The arm carries `output:` (README block 15 writes `output: QuoteSnapshot`
5247    /// in `on_chunk` and `output: VerifiedQuote` in `on_complete`), and `output:`
5248    /// is a step field with no flow-level position. Reusing
5249    /// [`Self::parse_step_body_into`] is therefore not a convenience — it is the
5250    /// only shape that accepts what the README publishes, and it means the arm
5251    /// dispatches through `run_step` like any other step.
5252    fn parse_stream_handler_arm(
5253        &mut self,
5254        name: &str,
5255        at: &Token,
5256    ) -> Result<StepNode, ParseError> {
5257        self.consume(TokenType::LBrace)?;
5258        let mut node = StepNode {
5259            name: name.to_string(),
5260            persona_ref: String::new(),
5261            given: String::new(),
5262            ask: String::new(),
5263            output_type: String::new(),
5264            confidence_floor: None,
5265            navigate_ref: String::new(),
5266            apply_ref: String::new(),
5267            requires_context: None,
5268            now_tz: None,
5269            guards: Vec::new(),
5270            pix_ops: Vec::new(),
5271            stream: None,
5272            performs: Vec::new(),
5273            loc: self.loc_of(at),
5274        };
5275        self.parse_step_body_into(&mut node)?;
5276        self.consume(TokenType::RBrace)?;
5277        Ok(node)
5278    }
5279
5280    fn parse_block_step(&mut self, _kw: &str) -> Result<Loc, ParseError> {
5281        let tok = self.current().clone();
5282        self.advance();
5283        // Skip optional arguments before brace
5284        while !self.check(TokenType::LBrace)
5285            && !self.check(TokenType::RBrace)
5286            && !self.check(TokenType::Eof)
5287            && !self.at_declaration_start()
5288        {
5289            self.advance();
5290        }
5291        if self.check(TokenType::LBrace) {
5292            self.skip_braced_block()?;
5293        }
5294        Ok(Loc {
5295            line: tok.line,
5296            column: tok.column,
5297        })
5298    }
5299
5300    /// v2.41.0 — parse `forge <Name>(seed: "<text>") -> <Type> { mode:,
5301    /// novelty:, depth:, branches:, constraints: }`. Real field capture
5302    /// (replacing the pre-v2.41.0 discard-everything stub). Strict closed-catalog:
5303    /// an unknown field is a hard parse error; all cross-field laws (Boden mode
5304    /// catalog, novelty range, depth/branches ≥ 1, `constraints:` → `anchor`)
5305    /// are v2.41.0 type-checker territory.
5306    fn parse_forge_step(&mut self) -> Result<ForgeBlock, ParseError> {
5307        let tok = self.consume(TokenType::Forge)?;
5308        let name = self.consume(TokenType::Identifier)?.value;
5309        let mut node = ForgeBlock {
5310            name,
5311            novelty: 0.5,
5312            depth: 1,
5313            branches: 1,
5314            loc: Loc { line: tok.line, column: tok.column },
5315            ..Default::default()
5316        };
5317        // `(seed: "...")`
5318        self.consume(TokenType::LParen)?;
5319        let arg = self.consume_any_ident_or_kw()?.value;
5320        self.consume(TokenType::Colon)?;
5321        if arg != "seed" {
5322            return Err(self.error(&format!(
5323                "forge '{}' expects `seed:` as its argument, found `{arg}`",
5324                node.name
5325            )));
5326        }
5327        node.seed = self.consume(TokenType::StringLit)?.value;
5328        self.consume(TokenType::RParen)?;
5329        // `-> <Type>`
5330        self.consume(TokenType::Arrow)?;
5331        node.output_type = self.consume_any_ident_or_kw()?.value;
5332        // `{ fields }`
5333        self.consume(TokenType::LBrace)?;
5334        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5335            let field = self.consume_any_ident_or_kw()?.value;
5336            self.consume(TokenType::Colon)?;
5337            match field.as_str() {
5338                "mode" => node.mode = self.consume_any_ident_or_kw()?.value,
5339                "novelty" => node.novelty = self.consume_number()?,
5340                "depth" => {
5341                    node.depth = self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0)
5342                }
5343                "branches" => {
5344                    node.branches =
5345                        self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0)
5346                }
5347                "constraints" => node.constraints_ref = self.consume_any_ident_or_kw()?.value,
5348                other => {
5349                    return Err(self.error(&format!("unknown forge field `{other}`")))
5350                }
5351            }
5352            if self.check(TokenType::Comma) {
5353                self.consume(TokenType::Comma)?;
5354            }
5355        }
5356        self.consume(TokenType::RBrace)?;
5357        Ok(node)
5358    }
5359
5360    /// v2.15.0 — Parse `par { stmt1 stmt2 … }` into CONCURRENT branches.
5361    /// Each top-level flow statement inside the block is one branch (a
5362    /// single-statement body); they execute concurrently at runtime
5363    /// (`flow_dispatcher::parallel::run_branches_concurrently`). Before v2.15.0 the
5364    /// `par` body was skipped (`parse_block_step`), so the branches were lost
5365    /// and the handler ran as a stub. Multi-statement branches (grouping
5366    /// several steps into one sequential branch) are a future grammar
5367    /// extension; today the natural `par { step A  step B }` fans A and B out.
5368    fn parse_par_block(&mut self) -> Result<ParBlock, ParseError> {
5369        let tok = self.current().clone();
5370        self.advance(); // consume `par`
5371        self.consume(TokenType::LBrace)?;
5372        let mut branches: Vec<Vec<FlowStep>> = Vec::new();
5373        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5374            branches.push(vec![self.parse_flow_step()?]);
5375        }
5376        self.consume(TokenType::RBrace)?;
5377        Ok(ParBlock {
5378            branches,
5379            loc: Loc {
5380                line: tok.line,
5381                column: tok.column,
5382            },
5383        })
5384    }
5385
5386    /// v2.4.0 — Parse the `quant` cognitive block surface.
5387    ///
5388    /// Grammar (the attribute header is OPTIONAL):
5389    /// ```text
5390    /// quant { <flow steps> }
5391    /// quant(encoding: amplitude, observable: M, qubits: 10,
5392    ///       depth: 4, bandwidth: 0.5, reupload: 3, backend: quant_sim) { <flow steps> }
5393    /// ```
5394    /// The bare form (the paper's example) leaves every attribute defaulted
5395    /// (`encoding = amplitude`, `effect = quant_sim`). The body is parsed into
5396    /// real nested `FlowStep`s — like `par` branches — so v2.4.0's Continuous
5397    /// Type Invariant scans actual AST rather than skipped tokens.
5398    /// v2.43.0 — parse `warden(<target>) within <Scope> { <body> }`. The
5399    /// `within <Scope>` clause is MANDATORY at the grammar level (fail-closed by
5400    /// construction: a scopeless warden cannot be written); v2.43.0 checks the
5401    /// scope RESOLVES + the target is in its allowlist.
5402    fn parse_warden(&mut self) -> Result<WardenBlock, ParseError> {
5403        let tok = self.consume(TokenType::Warden)?;
5404        // `(<target>)` — the resource under analysis.
5405        self.consume(TokenType::LParen)?;
5406        let target = self.consume_any_ident_or_kw()?.value;
5407        self.consume(TokenType::RParen)?;
5408        // `within <Scope>` — MANDATORY. Omitting it is a hard parse error.
5409        self.consume(TokenType::Within)?;
5410        let scope_ref = self.consume(TokenType::Identifier)?.value;
5411        let mut block = WardenBlock {
5412            target,
5413            scope_ref,
5414            body: Vec::new(),
5415            loc: Loc {
5416                line: tok.line,
5417                column: tok.column,
5418            },
5419        };
5420        // Body: real nested flow steps (like `quant`/`par`).
5421        self.consume(TokenType::LBrace)?;
5422        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5423            block.body.push(self.parse_flow_step()?);
5424        }
5425        self.consume(TokenType::RBrace)?;
5426        Ok(block)
5427    }
5428
5429    /// v2.43.0 — parse `scope <Name> { targets: [ … ], depth: <ident>,
5430    /// approver: [requires] "<cap>" }`. Flat key:value block (the `cache` shape).
5431    /// Catalog + non-empty validation is v2.43.0. Unknown fields are a hard error
5432    ///: a scope governs an offensive-capable analysis.
5433    fn parse_scope(&mut self) -> Result<ScopeDefinition, ParseError> {
5434        let tok = self.consume(TokenType::Scope)?;
5435        let name = self.consume(TokenType::Identifier)?.value;
5436        let mut node = ScopeDefinition {
5437            name,
5438            loc: Loc {
5439                line: tok.line,
5440                column: tok.column,
5441            },
5442            ..Default::default()
5443        };
5444        self.consume(TokenType::LBrace)?;
5445        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5446            let key = self.consume_any_ident_or_kw()?.value;
5447            self.consume(TokenType::Colon)?;
5448            match key.as_str() {
5449                "targets" => {
5450                    self.consume(TokenType::LBracket)?;
5451                    while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
5452                        let t = if self.check(TokenType::StringLit) {
5453                            self.consume(TokenType::StringLit)?.value
5454                        } else {
5455                            self.consume_any_ident_or_kw()?.value
5456                        };
5457                        node.targets.push(t);
5458                        if self.check(TokenType::Comma) {
5459                            self.advance();
5460                        }
5461                    }
5462                    self.consume(TokenType::RBracket)?;
5463                }
5464                "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
5465                "approver" => {
5466                    // Optional `requires` sugar before the capability string.
5467                    if self.current().value == "requires" {
5468                        self.advance();
5469                    }
5470                    node.approver = self.consume(TokenType::StringLit)?.value;
5471                }
5472                other => {
5473                    return Err(self.error(&format!(
5474                        "unknown scope field `{other}` in scope `{}` — expected \
5475                         `targets` / `depth` / `approver`",
5476                        node.name
5477                    )))
5478                }
5479            }
5480            if self.check(TokenType::Comma) {
5481                self.consume(TokenType::Comma)?;
5482            }
5483        }
5484        self.consume(TokenType::RBrace)?;
5485        Ok(node)
5486    }
5487
5488    fn parse_quant(&mut self) -> Result<QuantBlock, ParseError> {
5489        let tok = self.current().clone();
5490        self.advance(); // consume `quant`
5491
5492        let mut block = QuantBlock {
5493            encoding: None,
5494            observable: None,
5495            qubits: None,
5496            depth: None,
5497            bandwidth: None,
5498            reupload: None,
5499            // D1/D9 default backend: the CPU simulator effect. `qpu_native` is
5500            // opt-in via `backend: qpu_native`.
5501            effect: "quant_sim".to_string(),
5502            body: Vec::new(),
5503            loc: Loc {
5504                line: tok.line,
5505                column: tok.column,
5506            },
5507        };
5508
5509        // ── Optional attribute header: `(key: value, …)` ──
5510        if self.check(TokenType::LParen) {
5511            self.advance();
5512            while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
5513                let key = self.consume_any_ident_or_kw()?.value;
5514                self.consume(TokenType::Colon)?;
5515                match key.as_str() {
5516                    "encoding" => {
5517                        block.encoding = Some(self.consume_any_ident_or_kw()?.value)
5518                    }
5519                    "observable" => {
5520                        block.observable = Some(self.parse_dotted_identifier()?)
5521                    }
5522                    "qubits" => block.qubits = Some(self.consume_number()? as i64),
5523                    "depth" => block.depth = Some(self.consume_number()? as i64),
5524                    "bandwidth" => block.bandwidth = Some(self.consume_number()?),
5525                    // v2.23.0 — data re-uploading layers.
5526                    "reupload" => block.reupload = Some(self.consume_number()? as i64),
5527                    // `backend:` selects the algebraic-effect tag (D1/D9).
5528                    "backend" => block.effect = self.consume_any_ident_or_kw()?.value,
5529                    other => {
5530                        return Err(ParseError {
5531                            message: format!(
5532                                "Unknown `quant` attribute `{other}` — expected one of \
5533                                 encoding, observable, qubits, depth, bandwidth, reupload, backend"
5534                            ),
5535                            line: self.current().line,
5536                            column: self.current().column,
5537                            ..Default::default()
5538                        });
5539                    }
5540                }
5541                // Optional comma between attributes (order-free, trailing-comma ok).
5542                if self.check(TokenType::Comma) {
5543                    self.advance();
5544                }
5545            }
5546            self.consume(TokenType::RParen)?;
5547        }
5548
5549        // ── Body: real nested flow steps (like `par`) ──
5550        self.consume(TokenType::LBrace)?;
5551        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5552            block.body.push(self.parse_flow_step()?);
5553        }
5554        self.consume(TokenType::RBrace)?;
5555
5556        Ok(block)
5557    }
5558
5559    /// v2.4.0 — Parse the `yield <expr>` measurement point. Reuses the
5560    /// `let`-value expression grammar (reference / literal / arithmetic) so the
5561    /// yielded value's tokenization intent is preserved in `value_kind`.
5562    fn parse_yield(&mut self) -> Result<YieldStatement, ParseError> {
5563        let tok = self.consume(TokenType::Yield)?;
5564        let loc = self.loc_of(&tok);
5565        self.last_let_value_kind = "literal".to_string();
5566        let value_expr = self.parse_let_value_expr()?;
5567        Ok(YieldStatement {
5568            value_expr,
5569            value_kind: self.last_let_value_kind.clone(),
5570            loc,
5571        })
5572    }
5573
5574    /// Parse: keyword Name on target -> output_type (apply pattern).
5575    /// v2.67.0 — `compute <Name> on <a>, <b>, … -> <out>`.
5576    ///
5577    /// Positional arguments, bound to the compute's declared parameters in order.
5578    /// The generic [`Self::parse_apply_step`] captured a single `on <target>` and
5579    /// then the call site threw even that away (`arguments: Vec::new()`).
5580    fn parse_compute_apply(&mut self) -> Result<ComputeApplyStep, ParseError> {
5581        let tok = self.current().clone();
5582        let loc = self.loc_of(&tok);
5583        self.advance(); // consume `compute`
5584        let compute_name = self.consume_any_ident_or_kw()?.value.clone();
5585
5586        let mut arguments = Vec::new();
5587        if self.current().value == "on" {
5588            self.advance();
5589            loop {
5590                // v2.83.0 — SUBJECT position. README writes
5591                // `compute EligibilityScore on Profile.tenure, Profile.spend,
5592                // Profile.incidents -> score`; the bare-identifier read stopped
5593                // at the first dot, which is why every published `compute`
5594                // application failed on its own argument list.
5595                arguments.push(self.parse_subject()?);
5596                if self.check(TokenType::Comma) {
5597                    self.advance();
5598                } else {
5599                    break;
5600                }
5601            }
5602        }
5603
5604        let mut output_name = String::new();
5605        if self.check(TokenType::Arrow) {
5606            self.advance();
5607            output_name = self.consume_any_ident_or_kw()?.value.clone();
5608        }
5609
5610        Ok(ComputeApplyStep {
5611            compute_name,
5612            arguments,
5613            output_name,
5614            loc,
5615        })
5616    }
5617
5618    fn parse_apply_step(&mut self, _kw: &str) -> Result<(Loc, String, String, String), ParseError> {
5619        let tok = self.current().clone();
5620        self.advance(); // consume keyword
5621        let name = self.consume_any_ident_or_kw()?.value.clone();
5622        let mut target = String::new();
5623        let mut output_type = String::new();
5624        // "on" target
5625        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
5626            let next = self.current().clone();
5627            if next.value == "on" {
5628                self.advance();
5629                // v2.83.0 — SUBJECT position (the name before `on` is a
5630                // NAME and stays bare).
5631                target = self.parse_subject()?;
5632            }
5633        }
5634        // -> output_type
5635        if self.check(TokenType::Arrow) {
5636            self.advance();
5637            output_type = self.consume_any_ident_or_kw()?.value.clone();
5638        }
5639        // Skip optional braced block
5640        if self.check(TokenType::LBrace) {
5641            self.skip_braced_block()?;
5642        }
5643        Ok((
5644            Loc {
5645                line: tok.line,
5646                column: tok.column,
5647            },
5648            name,
5649            target,
5650            output_type,
5651        ))
5652    }
5653
5654    /// v2.83.0 — `<kind> <Name> [on <target>] [-> <binding>]` inside
5655    /// a `step { }` body.
5656    ///
5657    /// Differences from the flow-level `parse_apply_step`, both deliberate:
5658    ///
5659    /// - The target may be a CALL EXPRESSION, captured verbatim: README block
5660    ///   42 writes `mandate LegalPrecision on ContractDrafter(terms)`. The
5661    ///   flow-level form never needed this; the published step-level form does.
5662    /// - No trailing braced block is skipped. A guard is one statement; a
5663    /// silently-skipped block after it would be the v2.83.0 defect again.
5664    fn parse_step_guard(&mut self, kind: &str) -> Result<StepGuardNode, ParseError> {
5665        let tok = self.current().clone();
5666        self.advance(); // consume the keyword
5667        let name = self.consume_any_ident_or_kw()?.value.clone();
5668        let mut target = String::new();
5669        let mut binding = String::new();
5670        if self.current().value == "on" {
5671            self.advance();
5672            // v2.83.0 — SUBJECT position. `shield S on vital_event -> safe`
5673            // already worked; `shield S on Charge.output -> x` did not.
5674            target = self.parse_subject()?;
5675            // `ContractDrafter(terms)` — capture the balanced argument list
5676            // verbatim into the target string.
5677            if self.check(TokenType::LParen) {
5678                let mut depth = 0usize;
5679                loop {
5680                    let t = self.current().clone();
5681                    match t.ttype {
5682                        TokenType::LParen => depth += 1,
5683                        TokenType::RParen => depth -= 1,
5684                        TokenType::Eof => {
5685                            return Err(ParseError {
5686                                message: format!(
5687                                    "unterminated argument list in `{kind} {name} on {target}(…`"
5688                                ),
5689                                line: t.line,
5690                                column: t.column,
5691                                ..Default::default()
5692                            })
5693                        }
5694                        _ => {}
5695                    }
5696                    target.push_str(&t.value);
5697                    self.advance();
5698                    if depth == 0 {
5699                        break;
5700                    }
5701                }
5702            }
5703        }
5704        if self.check(TokenType::Arrow) {
5705            self.advance();
5706            binding = self.consume_any_ident_or_kw()?.value.clone();
5707        }
5708        Ok(StepGuardNode {
5709            kind: kind.to_string(),
5710            name,
5711            target,
5712            binding,
5713            loc: Loc {
5714                line: tok.line,
5715                column: tok.column,
5716            },
5717        })
5718    }
5719
5720    /// v2.83.0 — `reason [<target>] [{ given: … ask: "…" depth: N }]`.
5721    ///
5722    /// Replaces the `parse_flow_step_simple("reason")` call whose entire
5723    /// treatment of the block was `skip_braced_block()`. Sixteen README blocks
5724    /// write the braced form and every one of them lowered to an empty prompt.
5725    ///
5726    /// The field set is CLOSED. An unrecognised key is an ERROR that names the
5727    /// key and lists what is accepted — the v2.83.0 discipline: a skipped
5728    /// field in a deliberation removes the deliberation (a promptless `reason`
5729    /// is silent, not loud), so the silent direction is the dangerous one.
5730    fn parse_reason_step(&mut self) -> Result<ReasonStep, ParseError> {
5731        let tok = self.current().clone();
5732        let loc = self.loc_of(&tok);
5733        self.advance(); // consume `reason`
5734
5735        // The pre-v2.83.0 positional form: `reason <target>`. Absent when the
5736        // block follows immediately, which is how the README always writes it.
5737        //
5738        // The `Colon` lookahead matters: a bare `reason` on its own line inside
5739        // a `step { }` body is followed by the step's NEXT FIELD, and without
5740        // this guard the target would swallow that field's key (`output`) and
5741        // the step would then fail on a stray `:` — an error pointing two
5742        // tokens past the actual problem. `skip_flow_step_structural` used to
5743        // absorb this shape silently; a wrong diagnostic is not an improvement
5744        // on a silent drop.
5745        let next_is_field_key = self
5746            .tokens
5747            .get(self.pos + 1)
5748            .is_some_and(|t| t.ttype == TokenType::Colon);
5749        let target = if self.check(TokenType::LBrace)
5750            || self.at_declaration_start()
5751            || self.check(TokenType::RBrace)
5752            || self.check(TokenType::Eof)
5753            || next_is_field_key
5754        {
5755            String::new()
5756        } else {
5757            self.parse_dotted_identifier()?
5758        };
5759
5760        let mut node = ReasonStep {
5761            strategy: String::new(),
5762            target,
5763            given: String::new(),
5764            ask: String::new(),
5765            depth: None,
5766            loc,
5767        };
5768
5769        if !self.check(TokenType::LBrace) {
5770            return Ok(node);
5771        }
5772        self.advance();
5773        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5774            let key = self.current().clone();
5775            self.advance();
5776            self.consume(TokenType::Colon)?;
5777            match key.value.as_str() {
5778                // `given: A.output`, `given: A.output, sessions`,
5779                // `given: [baseline.topology, current.topology]` — all three
5780                // published shapes, normalised to one comma-joined string (the
5781                // same carrier `StepNode.given` already uses).
5782                "given" => {
5783                    let mut parts = vec![self.parse_expression_string()?];
5784                    while self.check(TokenType::Comma) {
5785                        self.advance();
5786                        parts.push(self.parse_expression_string()?);
5787                    }
5788                    node.given = parts.join(", ");
5789                }
5790                "ask" => node.ask = self.consume(TokenType::StringLit)?.value,
5791                "depth" => {
5792                    let n = self.current().clone();
5793                    if n.ttype != TokenType::Integer {
5794                        return Err(ParseError {
5795                            message: format!(
5796                                "`depth:` in a `reason` block is a deliberation depth — a \
5797                                 positive integer (got '{}')",
5798                                n.value
5799                            ),
5800                            line: n.line,
5801                            column: n.column,
5802                            ..Default::default()
5803                        });
5804                    }
5805                    self.advance();
5806                    node.depth = n.value.parse::<u32>().ok();
5807                }
5808                // `chain_of_thought: enabled` is the README's spelling of a
5809                // named strategy; `strategy: <name>` is the general form. Both
5810                // land in the same field because dispatch reads one posture.
5811                "chain_of_thought" => {
5812                    let v = self.consume_any_ident_or_kw()?.value;
5813                    if v == "enabled" {
5814                        node.strategy = "chain_of_thought".to_string();
5815                    }
5816                }
5817                "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value,
5818                // `target:` is the SUBJECT — the same field the positional
5819                // `reason <target>` form fills, spelled as a key. The parity
5820                // corpus writes it (`reason about_policy { target: "…" }`) and
5821                // the block was discarded whole, so the key has never meant
5822                // anything. Giving it BOTH ways is refused rather than resolved
5823                // by fiat: two spellings of one field with different values
5824                // have no defined winner, and picking one silently is how a
5825                // program comes to mean something its author did not write.
5826                "target" => {
5827                    let v = if self.check(TokenType::StringLit) {
5828                        self.consume(TokenType::StringLit)?.value
5829                    } else {
5830                        self.parse_dotted_identifier()?
5831                    };
5832                    if !node.target.is_empty() {
5833                        return Err(ParseError {
5834                            message: format!(
5835                                "`reason {} {{ target: … }}` declares the subject twice — \
5836                                 once positionally as `{}` and once as `target: {}`. They \
5837                                 are the same field. Write one of them.",
5838                                node.target, node.target, v
5839                            ),
5840                            line: key.line,
5841                            column: key.column,
5842                            ..Default::default()
5843                        });
5844                    }
5845                    node.target = v;
5846                }
5847                other => {
5848                    return Err(ParseError {
5849                        message: format!(
5850                            "unknown field '{other}' in a `reason` block. Accepted: given, \
5851                             ask, depth, strategy, chain_of_thought, target. A field this \
5852                             block does not recognise is REFUSED rather than skipped — a \
5853                             `reason` that silently loses its `ask:` deliberates over \
5854                             nothing, and that failure is quiet."
5855                        ),
5856                        line: key.line,
5857                        column: key.column,
5858                        ..Default::default()
5859                    })
5860                }
5861            }
5862        }
5863        self.consume(TokenType::RBrace)?;
5864        Ok(node)
5865    }
5866
5867    /// v2.83.0 — the CLOSED braceless catalog for `weave`.
5868    ///
5869    /// `output` is deliberately ABSENT, for the reason `at_navigate_field`
5870    /// already records: in step-body position `output:` is the STEP's own
5871    /// field, and a shared name makes the terminator ambiguous. This is not
5872    /// hypothetical here — it is the exact bug the old skipper had, from the
5873    /// other side: `skip_flow_step_structural` STOPPED at `output`, mid-list,
5874    /// and the step then failed on a stray comma.
5875    fn at_weave_field(&self) -> bool {
5876        const FIELDS: &[&str] = &["format", "include", "priority", "style"];
5877        self.field_ahead(FIELDS)
5878    }
5879
5880    /// v2.83.0 — `weave [a, b] [into <T>] [format: … include: […]]`.
5881    ///
5882    /// Three published surfaces, one implementation:
5883    ///   - the step-body statement — `weave [A.output, B.output]` followed by a
5884    ///     braceless `format:` / `include:` list (14 README blocks);
5885    ///   - the flow-body statement — `weave [A, B] into Report { format: T }`;
5886    ///   - the braced field form `weave { sources: […] … }`, which no published
5887    /// block writes but which predates this cycle and keeps working.
5888    fn parse_weave_step(&mut self) -> Result<FlowStep, ParseError> {
5889        let tok = self.current().clone();
5890        self.advance();
5891        let mut node = WeaveStep {
5892            sources: Vec::new(),
5893            target: String::new(),
5894            format_type: String::new(),
5895            priority: Vec::new(),
5896            style: String::new(),
5897            include: Vec::new(),
5898            loc: Loc {
5899                line: tok.line,
5900                column: tok.column,
5901            },
5902        };
5903        // `weave [A.output, B.output]` — the sources are REFERENCES, so they
5904        // are dotted. `parse_bracketed_dot_identifiers` is the same helper
5905        // `given:` uses; the pre-v2.83.0 braced form's `sources:` used the
5906        // non-dotted one, which is why a dotted source never had a spelling
5907        // that reached the AST.
5908        if self.check(TokenType::LBracket) {
5909            node.sources = self.parse_bracketed_dot_identifiers()?;
5910        } else if self.current().ttype == TokenType::Identifier
5911            && !self
5912                .tokens
5913                .get(self.pos + 1)
5914                .is_some_and(|t| t.ttype == TokenType::Colon)
5915        {
5916            // `weave Baz` — the bare positional subject every other statement
5917            // in the language takes (`probe X`, `reason X`, `validate X`), read
5918            // here as a one-element source list. It is the uniform rule, not a
5919            // special case, and it keeps parsing the shape that used to vanish
5920            // into `skip_flow_step_structural`.
5921            //
5922            // The Colon lookahead is the same guard `parse_reason_step` needs:
5923            // without it a bare `weave` would swallow the enclosing step's next
5924            // field KEY as its source.
5925            node.sources = vec![self.parse_dotted_identifier()?];
5926        }
5927        // `into <Target>` — the flow-level form's destination binding.
5928        if self.check(TokenType::Into) || self.current().value == "into" {
5929            self.advance();
5930            node.target = self.parse_dotted_identifier()?;
5931        }
5932        // The braceless continuation, terminated by the closed field catalog.
5933        while self.at_weave_field() {
5934            let f = self.current().value.clone();
5935            self.advance();
5936            self.consume(TokenType::Colon)?;
5937            match f.as_str() {
5938                "format" => node.format_type = self.consume_any_ident_or_kw()?.value.clone(),
5939                "include" => node.include = self.parse_bracketed_dot_identifiers()?,
5940                "priority" => node.priority = self.parse_bracketed_dot_identifiers()?,
5941                "style" => node.style = self.consume_any_ident_or_kw()?.value.clone(),
5942                // `at_weave_field` is the gate above; this arm is unreachable
5943                // unless the two catalogs drift apart.
5944                other => {
5945                    return Err(ParseError {
5946                        message: format!(
5947                            "`{other}` passed the `weave` field test but has no handler — \
5948                             the braceless catalog and its parser have drifted apart."
5949                        ),
5950                        line: tok.line,
5951                        column: tok.column,
5952                        ..Default::default()
5953                    })
5954                }
5955            }
5956        }
5957        if self.check(TokenType::LBrace) {
5958            self.advance();
5959            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5960                let f = self.current().value.clone();
5961                self.advance();
5962                if self.check(TokenType::Colon) {
5963                    self.advance();
5964                    match f.as_str() {
5965                        "sources" => node.sources = self.parse_bracketed_dot_identifiers()?,
5966                        "target" => node.target = self.consume_any_ident_or_kw()?.value.clone(),
5967                        "format" => {
5968                            node.format_type = self.consume_any_ident_or_kw()?.value.clone()
5969                        }
5970                        "priority" => node.priority = self.parse_bracketed_dot_identifiers()?,
5971                        "style" => node.style = self.consume_any_ident_or_kw()?.value.clone(),
5972                        // v2.83.0 — the braced form takes `include:` too,
5973                        // so the two spellings of one construct cannot disagree
5974                        // about which fields exist.
5975                        "include" => node.include = self.parse_bracketed_dot_identifiers()?,
5976                        _ => self.skip_value(),
5977                    }
5978                }
5979            }
5980            if self.check(TokenType::RBrace) {
5981                self.advance();
5982            }
5983        }
5984        Ok(FlowStep::Weave(node))
5985    }
5986
5987    fn parse_use_step(&mut self) -> Result<FlowStep, ParseError> {
5988        let tok = self.current().clone();
5989        self.advance();
5990        let tool_name = self.consume_any_ident_or_kw()?.value.clone();
5991        // v2.8.0 — two mutually-exclusive `use` argument surfaces:
5992        //   * `use Tool(query = "${q}", max_results = 5)` — D2 canonical
5993        // multi-field keyword args (v2.8.0 `UseArgs::Named`).
5994        // * `use Tool on "${arg}"` / `on query` — the v2.7.0 single positional
5995        //     argument (D5 back-compat, `UseArgs::LegacyPositional`):
5996        //       - a STRING LITERAL carrying interpolation (`on "${query}"`)
5997        //         resolved at dispatch against request-bound flow params;
5998        //       - a BARE identifier / literal (`on query` / `on 42`) verbatim.
5999        //     (Unquoted `${query}` is intentionally NOT a form — interpolation
6000        //     lives inside string literals everywhere in Axon.)
6001        let args = if self.check(TokenType::LParen) {
6002            UseArgs::Named(self.parse_named_arg_list()?)
6003        } else {
6004            let mut argument = String::new();
6005            if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6006                let next = self.current().clone();
6007                if next.value == "on" {
6008                    self.advance();
6009                    argument = self.consume_any_ident_or_kw()?.value.clone();
6010                }
6011            }
6012            UseArgs::LegacyPositional(argument)
6013        };
6014        if self.check(TokenType::LBrace) {
6015            self.skip_braced_block()?;
6016        }
6017        Ok(FlowStep::UseTool(UseToolStep {
6018            tool_name,
6019            args,
6020            loc: Loc {
6021                line: tok.line,
6022                column: tok.column,
6023            },
6024        }))
6025    }
6026
6027    /// v2.8.0 — parse `(name = value, …)` keyword args for the canonical
6028    /// `use Tool(...)` multi-field dispatch. Values are captured as expression
6029    /// strings (StringLit / Integer / Float / Bool / dotted identifier / list)
6030    /// via the shared `parse_let_atom`, since the frontend has no structured
6031    /// `Expr`. A trailing comma is tolerated; `()` yields no args.
6032    fn parse_named_arg_list(&mut self) -> Result<Vec<(String, String, String)>, ParseError> {
6033        self.consume(TokenType::LParen)?;
6034        let mut args = Vec::new();
6035        while !self.check(TokenType::RParen) {
6036            // Accept a keyword-as-name (`filter`, `type`, `from`, …) — real
6037            // adopter schemas use such names; the following `=` disambiguates.
6038            let name = self.consume_any_ident_or_kw()?.value;
6039            self.consume(TokenType::Assign)?;
6040            let value = self.parse_let_atom()?;
6041            // v2.10.0 — `parse_let_atom` classified the value (`"literal"` vs
6042            // `"reference"`); carry it so the runtime resolves a bare
6043            // identifier / `Step.output` as a binding lookup, not a literal.
6044            let value_kind = self.last_let_value_kind.clone();
6045            args.push((name, value, value_kind));
6046            if self.check(TokenType::Comma) {
6047                self.advance();
6048            } else {
6049                break;
6050            }
6051        }
6052        self.consume(TokenType::RParen)?;
6053        Ok(args)
6054    }
6055
6056    fn parse_remember_step(&mut self) -> Result<FlowStep, ParseError> {
6057        let tok = self.current().clone();
6058        self.advance();
6059        let expr = self.consume_any_ident_or_kw()?.value.clone();
6060        let mut mem = String::new();
6061        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6062            let next = self.current().clone();
6063            if next.value == "in" || next.ttype == TokenType::In {
6064                self.advance();
6065                mem = self.consume_any_ident_or_kw()?.value.clone();
6066            }
6067        }
6068        Ok(FlowStep::Remember(RememberStep {
6069            expression: expr,
6070            memory_target: mem,
6071            loc: Loc {
6072                line: tok.line,
6073                column: tok.column,
6074            },
6075        }))
6076    }
6077
6078    fn parse_recall_step(&mut self) -> Result<FlowStep, ParseError> {
6079        let tok = self.current().clone();
6080        self.advance();
6081        let query = if self.check(TokenType::StringLit) {
6082            self.consume(TokenType::StringLit)?.value.clone()
6083        } else {
6084            self.consume_any_ident_or_kw()?.value.clone()
6085        };
6086        let mut mem = String::new();
6087        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6088            let next = self.current().clone();
6089            if next.value == "from" || next.ttype == TokenType::From {
6090                self.advance();
6091                mem = self.consume_any_ident_or_kw()?.value.clone();
6092            }
6093        }
6094        Ok(FlowStep::Recall(RecallStep {
6095            query,
6096            memory_source: mem,
6097            loc: Loc {
6098                line: tok.line,
6099                column: tok.column,
6100            },
6101        }))
6102    }
6103
6104    fn parse_hibernate_step(&mut self) -> Result<FlowStep, ParseError> {
6105        let tok = self.current().clone();
6106        self.advance();
6107        let mut event = String::new();
6108        let mut timeout = String::new();
6109        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6110            // v2.83.0 — README III writes `hibernate until "event_name"`
6111            // (the `until` keyword + a STRING event). The parser accepted only
6112            // the bare-identifier form, so the published block never compiled.
6113            // Both forms resolve to the same field.
6114            let first = self.consume_any_ident_or_kw()?.value.clone();
6115            if first == "until" && self.check(TokenType::StringLit) {
6116                event = self.consume(TokenType::StringLit)?.value.clone();
6117            } else {
6118                event = first;
6119            }
6120        }
6121        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6122            let next = self.current().clone();
6123            if next.ttype == TokenType::Duration {
6124                self.advance();
6125                timeout = next.value.clone();
6126            }
6127        }
6128        Ok(FlowStep::Hibernate(HibernateStep {
6129            event_name: event,
6130            timeout,
6131            loc: Loc {
6132                line: tok.line,
6133                column: tok.column,
6134            },
6135        }))
6136    }
6137
6138    /// v2.63.0 — `focus <Dataspace> { where: "<filter>", select: [cols], as: <name> }`
6139    /// — σ_φ ∘ π_v over a declared dataspace. The `where:` string is the
6140    /// v1.30.0 data-plane filter grammar (the design decision, shared with retrieve /
6141    /// navigate). Pre-108.d the optional body was silently discarded.
6142    /// v2.65.0 — `grad <letName> wrt <x> [as <name>]` /
6143    /// `grad <letName> wrt [a, b] as <name>`. The differentiation itself
6144    /// happens at CHECK/IR time (T931/T932 + the symbolic differentiator);
6145    /// the parser only captures the surface.
6146    fn parse_grad_step(&mut self) -> Result<FlowStep, ParseError> {
6147        let tok = self.current().clone();
6148        self.advance();
6149        let target = self.consume_any_ident_or_kw()?.value.clone();
6150        let mut wrt: Vec<String> = Vec::new();
6151        let mut output = String::new();
6152        if !self.at_declaration_start() && self.current().value == "wrt" {
6153            self.advance();
6154            if self.check(TokenType::LBracket) {
6155                wrt = self.parse_bracketed_identifiers()?;
6156            } else {
6157                wrt.push(self.consume_any_ident_or_kw()?.value.clone());
6158            }
6159        }
6160        if !self.at_declaration_start() && self.current().value == "as" {
6161            self.advance();
6162            output = self.consume_any_ident_or_kw()?.value.clone();
6163        }
6164        Ok(FlowStep::Grad(GradStep {
6165            target,
6166            wrt,
6167            output,
6168            loc: Loc {
6169                line: tok.line,
6170                column: tok.column,
6171            },
6172        }))
6173    }
6174
6175    fn parse_focus_step(&mut self) -> Result<FlowStep, ParseError> {
6176        let tok = self.current().clone();
6177        self.advance();
6178        let expression = if self.at_declaration_start()
6179            || self.check(TokenType::RBrace)
6180            || self.check(TokenType::Eof)
6181        {
6182            String::new()
6183        } else {
6184            self.consume_any_ident_or_kw()?.value.clone()
6185        };
6186        let mut where_expr = String::new();
6187        let mut select: Vec<String> = Vec::new();
6188        let mut output = String::new();
6189        if self.check(TokenType::LBrace) {
6190            self.advance();
6191            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6192                if self.check(TokenType::Comma) {
6193                    self.advance();
6194                    continue;
6195                }
6196                let f = self.current().value.clone();
6197                self.advance();
6198                if self.check(TokenType::Colon) {
6199                    self.advance();
6200                    match f.as_str() {
6201                        "where" => {
6202                            where_expr = self.consume(TokenType::StringLit)?.value.clone()
6203                        }
6204                        "select" => select = self.parse_bracketed_identifiers()?,
6205                        "as" | "alias" => {
6206                            output = self.consume_any_ident_or_kw()?.value.clone()
6207                        }
6208                        _ => self.skip_value(),
6209                    }
6210                }
6211            }
6212            if self.check(TokenType::RBrace) {
6213                self.advance();
6214            }
6215        }
6216        Ok(FlowStep::Focus(FocusStep {
6217            expression,
6218            where_expr,
6219            select,
6220            output,
6221            loc: Loc {
6222                line: tok.line,
6223                column: tok.column,
6224            },
6225        }))
6226    }
6227
6228    fn parse_associate_step(&mut self) -> Result<FlowStep, ParseError> {
6229        let tok = self.current().clone();
6230        self.advance();
6231        let left = self.consume_any_ident_or_kw()?.value.clone();
6232        let mut right = String::new();
6233        let mut using = String::new();
6234        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6235            right = self.consume_any_ident_or_kw()?.value.clone();
6236        }
6237        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6238            let next = self.current().clone();
6239            if next.value == "using" {
6240                self.advance();
6241                using = self.consume_any_ident_or_kw()?.value.clone();
6242            }
6243        }
6244        let mut output = String::new();
6245        if self.check(TokenType::LBrace) {
6246            self.advance();
6247            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6248                let f = self.current().value.clone();
6249                self.advance();
6250                if self.check(TokenType::Colon) {
6251                    self.advance();
6252                    match f.as_str() {
6253                        "as" | "alias" => output = self.consume_any_ident_or_kw()?.value.clone(),
6254                        _ => self.skip_value(),
6255                    }
6256                }
6257            }
6258            if self.check(TokenType::RBrace) {
6259                self.advance();
6260            }
6261        }
6262        Ok(FlowStep::Associate(AssociateStep {
6263            left,
6264            right,
6265            using_field: using,
6266            output,
6267            loc: Loc {
6268                line: tok.line,
6269                column: tok.column,
6270            },
6271        }))
6272    }
6273
6274    fn parse_aggregate_step(&mut self) -> Result<FlowStep, ParseError> {
6275        let tok = self.current().clone();
6276        self.advance();
6277        let target = self.consume_any_ident_or_kw()?.value.clone();
6278        let mut group_by = Vec::new();
6279        let mut alias = String::new();
6280        let mut compute: Vec<String> = Vec::new();
6281        let mut where_expr = String::new();
6282        if self.check(TokenType::LBrace) {
6283            self.advance();
6284            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6285                let f = self.current().value.clone();
6286                self.advance();
6287                if self.check(TokenType::Colon) {
6288                    self.advance();
6289                    match f.as_str() {
6290                        "group_by" => group_by = self.parse_bracketed_identifiers()?,
6291                        "alias" | "as" => alias = self.consume_any_ident_or_kw()?.value.clone(),
6292                        // v2.63.0 — the closed aggregate catalog, kept
6293                        // RAW (`count`, `sum(score)`, …); T930 validates.
6294                        "compute" => compute = self.parse_bracketed_aggregates()?,
6295                        // v2.63.0 — the data-plane where.
6296                        "where" => where_expr = self.consume(TokenType::StringLit)?.value.clone(),
6297                        _ => self.skip_value(),
6298                    }
6299                }
6300            }
6301            if self.check(TokenType::RBrace) {
6302                self.advance();
6303            }
6304        }
6305        Ok(FlowStep::Aggregate(AggregateStep {
6306            target,
6307            group_by,
6308            alias,
6309            compute,
6310            where_expr,
6311            loc: Loc {
6312                line: tok.line,
6313                column: tok.column,
6314            },
6315        }))
6316    }
6317
6318    fn parse_explore_step(&mut self) -> Result<FlowStep, ParseError> {
6319        let tok = self.current().clone();
6320        self.advance();
6321        let target = self.consume_any_ident_or_kw()?.value.clone();
6322        let mut limit = None;
6323        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6324            if self.current().ttype == TokenType::Integer {
6325                limit = self.current().value.parse::<i64>().ok();
6326                self.advance();
6327            }
6328        }
6329        let mut output = String::new();
6330        if self.check(TokenType::LBrace) {
6331            self.advance();
6332            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6333                let f = self.current().value.clone();
6334                self.advance();
6335                if self.check(TokenType::Colon) {
6336                    self.advance();
6337                    match f.as_str() {
6338                        "as" | "alias" => output = self.consume_any_ident_or_kw()?.value.clone(),
6339                        _ => self.skip_value(),
6340                    }
6341                }
6342            }
6343            if self.check(TokenType::RBrace) {
6344                self.advance();
6345            }
6346        }
6347        Ok(FlowStep::ExploreStep(ExploreStepNode {
6348            target,
6349            limit,
6350            output,
6351            loc: Loc {
6352                line: tok.line,
6353                column: tok.column,
6354            },
6355        }))
6356    }
6357
6358    /// v2.63.0 — parse `[count, sum(score), avg(x)]`: bracketed
6359    /// aggregate entries, each `ident` or `ident(ident)`, kept raw.
6360    fn parse_bracketed_aggregates(&mut self) -> Result<Vec<String>, ParseError> {
6361        let mut out = Vec::new();
6362        self.consume(TokenType::LBracket)?;
6363        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
6364            let name = self.consume_any_ident_or_kw()?.value.clone();
6365            if self.check(TokenType::LParen) {
6366                self.advance();
6367                let col = self.consume_any_ident_or_kw()?.value.clone();
6368                self.consume(TokenType::RParen)?;
6369                out.push(format!("{name}({col})"));
6370            } else {
6371                out.push(name);
6372            }
6373            if self.check(TokenType::Comma) {
6374                self.advance();
6375            }
6376        }
6377        self.consume(TokenType::RBracket)?;
6378        Ok(out)
6379    }
6380
6381    /// v2.63.0 — the governed ingest step:
6382    ///
6383    /// ```text
6384    /// ingest <sourceRef> into <Dataspace> {
6385    ///     format: csv | json
6386    ///     limits { max_bytes: N, max_rows: N }
6387    /// }
6388    /// ```
6389    ///
6390    /// Until 108.c the body was consumed by `skip_braced_block()`. Now it
6391    /// is a closed grammar: `format:` (raw here; required + validated by
6392    /// `axon-T929`) and an optional `limits { … }` block whose bounds are
6393    /// enforced on the raw byte stream BEFORE parsing. An unknown
6394    /// body entry is a parse error.
6395    fn parse_ingest_step(&mut self) -> Result<FlowStep, ParseError> {
6396        let tok = self.current().clone();
6397        self.advance();
6398        let source = self.consume_any_ident_or_kw()?.value.clone();
6399        let mut target = String::new();
6400        let mut format = String::new();
6401        let mut max_bytes: Option<u64> = None;
6402        let mut max_rows: Option<u64> = None;
6403        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6404            let next = self.current().clone();
6405            if next.value == "into" || next.ttype == TokenType::Into {
6406                self.advance();
6407                target = self.consume_any_ident_or_kw()?.value.clone();
6408            }
6409        }
6410        if self.check(TokenType::LBrace) {
6411            self.consume(TokenType::LBrace)?;
6412            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6413                // Optional separators between body entries.
6414                if self.check(TokenType::Comma) {
6415                    self.advance();
6416                    continue;
6417                }
6418                let entry = self.current().clone();
6419                match entry.value.as_str() {
6420                    "format" => {
6421                        self.advance();
6422                        self.consume(TokenType::Colon)?;
6423                        format = self.consume_any_ident_or_kw()?.value.clone();
6424                    }
6425                    "limits" => {
6426                        self.advance();
6427                        self.consume(TokenType::LBrace)?;
6428                        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6429                            let bound = self.current().clone();
6430                            self.advance();
6431                            self.consume(TokenType::Colon)?;
6432                            let num_tok = self.consume(TokenType::Integer)?.clone();
6433                            let value = num_tok.value.parse::<u64>().map_err(|_| ParseError {
6434                                message: format!(
6435                                    "ingest `limits` bound `{}` must be a non-negative \
6436                                     integer byte/row count, got `{}`.",
6437                                    bound.value, num_tok.value
6438                                ),
6439                                line: num_tok.line,
6440                                column: num_tok.column,
6441                                ..Default::default()
6442                            })?;
6443                            match bound.value.as_str() {
6444                                "max_bytes" => max_bytes = Some(value),
6445                                "max_rows" => max_rows = Some(value),
6446                                other => {
6447                                    return Err(ParseError {
6448                                        message: format!(
6449                                            "Unknown ingest limit `{other}`. The closed \
6450                                             limits grammar is `max_bytes: <N>` and \
6451                                             `max_rows: <N>` — bounds enforced on the raw \
6452                                             stream BEFORE parsing.",
6453                                        ),
6454                                        line: bound.line,
6455                                        column: bound.column,
6456                                        ..Default::default()
6457                                    });
6458                                }
6459                            }
6460                            if self.check(TokenType::Comma) {
6461                                self.advance();
6462                            }
6463                        }
6464                        self.consume(TokenType::RBrace)?;
6465                    }
6466                    other => {
6467                        return Err(ParseError {
6468                            message: format!(
6469                                "Unknown entry `{other}` in ingest body. The closed \
6470                                 grammar is `format: csv|json` and \
6471                                 `limits {{ max_bytes: <N>, max_rows: <N> }}`.",
6472                            ),
6473                            line: entry.line,
6474                            column: entry.column,
6475                            ..Default::default()
6476                        });
6477                    }
6478                }
6479            }
6480            self.consume(TokenType::RBrace)?;
6481        }
6482        Ok(FlowStep::Ingest(IngestStep {
6483            source,
6484            target,
6485            format,
6486            max_bytes,
6487            max_rows,
6488            loc: Loc {
6489                line: tok.line,
6490                column: tok.column,
6491            },
6492        }))
6493    }
6494
6495    /// v2.83.0 — is the cursor on a `navigate` field (`<name>:`)?
6496    ///
6497    /// The continuation test for the braceless field list. Closed catalog by
6498    /// construction: a name outside it ends the navigate and belongs to the
6499    /// enclosing step, which is exactly what makes the delimiter-free form
6500    /// unambiguous.
6501    fn at_navigate_field(&self) -> bool {
6502        const FIELDS: &[&str] = &[
6503            // v2.83.0 — `output` is deliberately ABSENT from the
6504            // BRACELESS catalog even though the braced form accepts it as an
6505            // alias for `as`. In step-body position `output:` is the STEP's
6506            // own field, and a shared name would make the terminator
6507            // ambiguous — the braceless navigate would swallow the step's
6508            // output type. README writes `as:` in this position throughout;
6509            // the braced/flow-level form keeps both spellings.
6510            "corpus", "query", "trail", "as", "from", "budget", "where",
6511            "depth", "recall",
6512        ];
6513        self.field_ahead(FIELDS)
6514    }
6515
6516    /// v2.83.0 — the same test for `drill`.
6517    fn at_drill_field(&self) -> bool {
6518        // Same reason as `at_navigate_field`: no `output` in the braceless
6519        // catalog, because that name belongs to the enclosing step.
6520        const FIELDS: &[&str] = &["subtree", "path", "query", "as"];
6521        self.field_ahead(FIELDS)
6522    }
6523
6524    /// `<one of names>` immediately followed by `:`.
6525    fn field_ahead(&self, names: &[&str]) -> bool {
6526        let cur = self.current();
6527        if !names.contains(&cur.value.as_str()) {
6528            return false;
6529        }
6530        self.tokens
6531            .get(self.pos + 1)
6532            .is_some_and(|t| t.ttype == TokenType::Colon)
6533    }
6534
6535    /// v2.83.0 — a CONFIG KEY: `"env:DATABASE_URL"` or the bare
6536    /// `env:DATABASE_URL` README publishes.
6537    ///
6538    /// v2.67.0 made `connection:`/`endpoint:` a config KEY rather than a URL or a
6539    /// DSN — the address resolves per deployment. README writes both the
6540    /// quoted and the bare spelling; the parser took only the quoted one, so
6541    /// every published `axonstore` with an unquoted key failed on its own
6542    /// third line. One value, two spellings — the epsilon/tolerance
6543    /// resolution of v2.83.0, applied to the config surface.
6544    fn parse_config_key(&mut self) -> Result<String, ParseError> {
6545        if self.check(TokenType::StringLit) {
6546            return Ok(self.consume(TokenType::StringLit)?.value.clone());
6547        }
6548        let scheme = self.consume_any_ident_or_kw()?.value.clone();
6549        if self.check(TokenType::Colon) {
6550            self.advance();
6551            let key = self.consume_any_ident_or_kw()?.value.clone();
6552            return Ok(format!("{scheme}:{key}"));
6553        }
6554        Ok(scheme)
6555    }
6556
6557    /// v2.83.0 — a PIX field value: a string literal OR a binding
6558    /// reference. README writes `query: question` (the flow parameter) far
6559    /// more often than a literal, and the parser accepted only the literal —
6560    /// which is why every published `navigate` failed on its own second line.
6561    fn parse_pix_value(&mut self) -> Result<String, ParseError> {
6562        if self.check(TokenType::StringLit) {
6563            return Ok(self.consume(TokenType::StringLit)?.value.clone());
6564        }
6565        Ok(self.consume_any_ident_or_kw()?.value.clone())
6566    }
6567
6568    fn parse_navigate_step(&mut self) -> Result<FlowStep, ParseError> {
6569        let tok = self.current().clone();
6570        self.advance();
6571        let pix_name = self.consume_any_ident_or_kw()?.value.clone();
6572        let mut node = NavigateStep {
6573            depth: None,
6574            pix_name,
6575            corpus_name: String::new(),
6576            query_expr: String::new(),
6577            trail_enabled: false,
6578            output_name: String::new(),
6579            seed: String::new(),
6580            budget: None,
6581            where_expr: String::new(),
6582            loc: Loc {
6583                line: tok.line,
6584                column: tok.column,
6585            },
6586        };
6587        // v2.83.0 — the BRACELESS field form, which is what README pix/
6588        // corpus publishes everywhere:
6589        //
6590        //     navigate ContractIndex
6591        //         query: question
6592        //         trail: enabled
6593        //         as: relevant_sections
6594        //
6595        // Terminated by the field-name set, not by a brace: the navigate
6596        // fields are a CLOSED catalog, so "the next token is one of these and
6597        // is followed by a colon" is an unambiguous continuation test. That is
6598        // the same closed-catalog discipline the rest of the language uses,
6599        // and it is why this form needs no delimiter to be parseable.
6600        if !self.check(TokenType::LBrace) {
6601            while self.at_navigate_field() {
6602                let f = self.current().value.clone();
6603                self.advance();
6604                self.consume(TokenType::Colon)?;
6605                match f.as_str() {
6606                    "corpus" => node.corpus_name = self.consume_any_ident_or_kw()?.value.clone(),
6607                    "query" => node.query_expr = self.parse_pix_value()?,
6608                    "trail" => {
6609                        let v = self.consume_any_ident_or_kw()?.value;
6610                        node.trail_enabled = matches!(v.as_str(), "true" | "enabled" | "on");
6611                    }
6612                    "output" | "as" => {
6613                        node.output_name = self.consume_any_ident_or_kw()?.value.clone()
6614                    }
6615                    "from" => node.seed = self.consume_any_ident_or_kw()?.value.clone(),
6616                    "budget" => node.budget = self.parse_optional_int(),
6617                    "where" => node.where_expr = self.parse_pix_value()?,
6618                    "depth" => node.depth = self.parse_optional_int(),
6619                    // v2.83.0 — `recall: episodic` selects the MDN memory
6620                    // mode README's clinical/legal examples write. The
6621                    // navigator's episodic path is v2.13.0's adaptive corpus
6622                    // reinforcement, keyed by the corpus declaration; the
6623                    // value is accepted and recorded on the seed so nothing
6624                    // is silently dropped, and the adaptive path already
6625                    // reads the corpus-level flag.
6626                    "recall" => {
6627                        let mode = self.consume_any_ident_or_kw()?.value.clone();
6628                        if node.seed.is_empty() {
6629                            node.seed = format!("recall:{mode}");
6630                        }
6631                    }
6632                    _ => self.skip_value(),
6633                }
6634            }
6635        }
6636        if self.check(TokenType::LBrace) {
6637            self.advance();
6638            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6639                let f = self.current().value.clone();
6640                self.advance();
6641                if self.check(TokenType::Colon) {
6642                    self.advance();
6643                    match f.as_str() {
6644                        "corpus" => {
6645                            node.corpus_name = self.consume_any_ident_or_kw()?.value.clone()
6646                        }
6647                        "query" => node.query_expr = self.parse_pix_value()?,
6648                        "trail" => {
6649                            let v = self.consume_any_ident_or_kw()?.value;
6650                            node.trail_enabled =
6651                                matches!(v.as_str(), "true" | "enabled" | "on");
6652                        }
6653                        "output" | "as" => {
6654                            node.output_name = self.consume_any_ident_or_kw()?.value.clone()
6655                        }
6656                        // v2.13.0 — MDN corpus-graph navigation.
6657                        "from" => node.seed = self.consume_any_ident_or_kw()?.value.clone(),
6658                        "budget" => node.budget = self.parse_optional_int(),
6659                        // v2.17.0 (Q2) — column-scoped navigation: a raw filter
6660                        // expr (mirrors `retrieve … where`) pushed to the SELECT
6661                        // that sources the corpus `documents:`/`relations:` rows,
6662                        // so a `corpus from axonstore` is scoped to a sub-tenant
6663                        // COLUMN (`where: "tenant_id == '${tenant_id}'"`), not just
6664                        // the axon-tenant RLS scope. Resolved by the v1.32.0 filter
6665                        // compiler at runtime (`${name}` → `$N` bind params).
6666                        "where" => {
6667                            node.where_expr = self.consume(TokenType::StringLit)?.value.clone()
6668                        }
6669                        _ => self.skip_value(),
6670                    }
6671                }
6672            }
6673            if self.check(TokenType::RBrace) {
6674                self.advance();
6675            }
6676        }
6677        Ok(FlowStep::Navigate(node))
6678    }
6679
6680    fn parse_drill_step(&mut self) -> Result<FlowStep, ParseError> {
6681        let tok = self.current().clone();
6682        self.advance();
6683        let pix_name = self.consume_any_ident_or_kw()?.value.clone();
6684        let mut node = DrillStep {
6685            pix_name,
6686            subtree_path: String::new(),
6687            query_expr: String::new(),
6688            output_name: String::new(),
6689            loc: Loc {
6690                line: tok.line,
6691                column: tok.column,
6692            },
6693        };
6694        // v2.83.0 — `drill <Ref> into "<path>" query: … as: …`, the form
6695        // README publishes. `into` is a positional keyword (no colon), the
6696        // rest is the same braceless closed-catalog field list as `navigate`.
6697        if self.current().value == "into" {
6698            self.advance();
6699            // v2.83.0 — README writes BOTH `into "Liabilities"` (a title)
6700            // and `into findings.top_region` (a dotted binding path). The
6701            // subtree path is dot-separated either way, so both spellings
6702            // land in the same field.
6703            node.subtree_path = if self.check(TokenType::StringLit) {
6704                self.consume(TokenType::StringLit)?.value.clone()
6705            } else {
6706                self.parse_dotted_identifier()?
6707            };
6708        }
6709        if !self.check(TokenType::LBrace) {
6710            while self.at_drill_field() {
6711                let f = self.current().value.clone();
6712                self.advance();
6713                self.consume(TokenType::Colon)?;
6714                match f.as_str() {
6715                    "subtree" | "path" => {
6716                        node.subtree_path = self.consume(TokenType::StringLit)?.value.clone()
6717                    }
6718                    "query" => node.query_expr = self.parse_pix_value()?,
6719                    "output" | "as" => {
6720                        node.output_name = self.consume_any_ident_or_kw()?.value.clone()
6721                    }
6722                    _ => self.skip_value(),
6723                }
6724            }
6725        }
6726        if self.check(TokenType::LBrace) {
6727            self.advance();
6728            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6729                let f = self.current().value.clone();
6730                self.advance();
6731                if self.check(TokenType::Colon) {
6732                    self.advance();
6733                    match f.as_str() {
6734                        "subtree" | "path" => {
6735                            node.subtree_path = self.consume(TokenType::StringLit)?.value.clone()
6736                        }
6737                        "query" => node.query_expr = self.parse_pix_value()?,
6738                        "output" | "as" => {
6739                            node.output_name = self.consume_any_ident_or_kw()?.value.clone()
6740                        }
6741                        _ => self.skip_value(),
6742                    }
6743                }
6744            }
6745            if self.check(TokenType::RBrace) {
6746                self.advance();
6747            }
6748        }
6749        Ok(FlowStep::Drill(node))
6750    }
6751
6752    fn parse_corroborate_step(&mut self) -> Result<FlowStep, ParseError> {
6753        let tok = self.current().clone();
6754        self.advance();
6755        let nav_ref = self.consume_any_ident_or_kw()?.value.clone();
6756        let mut output = String::new();
6757        if self.check(TokenType::Arrow) {
6758            self.advance();
6759            output = self.consume_any_ident_or_kw()?.value.clone();
6760        }
6761        Ok(FlowStep::Corroborate(CorroborateStep {
6762            navigate_ref: nav_ref,
6763            output_name: output,
6764            loc: Loc {
6765                line: tok.line,
6766                column: tok.column,
6767            },
6768        }))
6769    }
6770
6771    fn parse_listen_step(&mut self) -> Result<FlowStep, ParseError> {
6772        let tok = self.current().clone();
6773        self.advance();
6774        // v1.6.0 D4 — dual-mode listen:
6775        // • String topic (legacy, deprecated since v1.6.0)
6776        //   • Identifier (canonical: declared ChannelDefinition)
6777        let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
6778            (self.consume(TokenType::StringLit)?.value.clone(), false)
6779        } else {
6780            (self.consume_any_ident_or_kw()?.value.clone(), true)
6781        };
6782        let mut alias = String::new();
6783        if !self.at_declaration_start()
6784            && !self.check(TokenType::RBrace)
6785            && !self.check(TokenType::LBrace)
6786        {
6787            let next = self.current().clone();
6788            if next.value == "as" || next.ttype == TokenType::As {
6789                self.advance();
6790                alias = self.consume_any_ident_or_kw()?.value.clone();
6791            }
6792        }
6793        // v2.4.0 — parse the handler body into real flow-steps (was
6794        // `skip_braced_block`'d, leaving the listener inert). The body runs on
6795        // each event / scheduled tick.
6796        let body = self.parse_listener_body()?;
6797        Ok(FlowStep::Listen(ListenStep {
6798            channel,
6799            channel_is_ref,
6800            event_alias: alias,
6801            body,
6802            loc: Loc {
6803                line: tok.line,
6804                column: tok.column,
6805            },
6806        }))
6807    }
6808
6809    /// v2.4.0 — parse a `listen … { <flow steps> }` handler body. The body
6810    /// is OPTIONAL (a bodyless `listen channel` returns an empty Vec); when
6811    /// present, each statement is a real [`FlowStep`] (the same grammar as a
6812    /// flow / `quant` / `par` body), executed per trigger by the v2.4.0 runtime.
6813    fn parse_listener_body(&mut self) -> Result<Vec<FlowStep>, ParseError> {
6814        let mut body = Vec::new();
6815        if self.check(TokenType::LBrace) {
6816            self.advance(); // consume `{`
6817            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6818                body.push(self.parse_flow_step()?);
6819            }
6820            self.consume(TokenType::RBrace)?;
6821        }
6822        Ok(body)
6823    }
6824
6825    /// v2.83.0 — `retrieve [from] <Store> [where "<expr>"] [as <alias>]`
6826    /// alongside the pre-existing braced `retrieve <Store> { where: … as: … }`.
6827    ///
6828    /// README axonstore writes the braceless form with `from` and with `where`
6829    /// taking its argument DIRECTLY — no colon. Neither spelling parsed, so the
6830    /// only published `retrieve` failed on its own first line.
6831    fn parse_retrieve_step(&mut self) -> Result<FlowStep, ParseError> {
6832        let tok = self.current().clone();
6833        self.advance();
6834        // `from` is optional noise-with-meaning: it reads as English and the
6835        // store name carries the content either way.
6836        if self.check(TokenType::From) || self.current().value == "from" {
6837            self.advance();
6838        }
6839        let store = self.consume_any_ident_or_kw()?.value.clone();
6840        let mut where_expr = String::new();
6841        let mut alias = String::new();
6842        let mut order_by = String::new();
6843        let mut limit_expr = String::new();
6844        let mut aggregate = String::new();
6845        let mut group_by = String::new();
6846        let mut cache = String::new();
6847        // v2.83.0 — the BRACELESS clauses README publishes. Note they
6848        // take their argument with NO colon (`where "…"`, `as record`), which
6849        // is why the closed-catalog `field_ahead` test used elsewhere does not
6850        // apply: the terminator here is the clause keyword itself. Both names
6851        // are absent from the step-body field set, so a `retrieve` written
6852        // inside a step cannot swallow the step's own fields.
6853        loop {
6854            match self.current().value.as_str() {
6855                "where" if !self.check(TokenType::LBrace) => {
6856                    self.advance();
6857                    where_expr = self.consume(TokenType::StringLit)?.value.clone();
6858                }
6859                "as" => {
6860                    self.advance();
6861                    alias = self.consume_any_ident_or_kw()?.value.clone();
6862                }
6863                _ => break,
6864            }
6865        }
6866        if self.check(TokenType::LBrace) {
6867            self.advance();
6868            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6869                let f = self.current().value.clone();
6870                self.advance();
6871                if self.check(TokenType::Colon) {
6872                    self.advance();
6873                    match f.as_str() {
6874                        "where" => where_expr = self.consume(TokenType::StringLit)?.value.clone(),
6875                        "as" | "alias" => alias = self.consume_any_ident_or_kw()?.value.clone(),
6876                        // v2.21.0 — `order_by:` is a string literal
6877                        // (`"col asc, col2 desc"`), same surface as `where:`.
6878                        "order_by" => {
6879                            order_by = self.consume(TokenType::StringLit)?.value.clone()
6880                        }
6881                        // v2.21.0 — `limit:` is a bare integer literal
6882                        // (`limit: 100`) OR a string carrying a binding
6883                        // (`limit: "${max}"`). Captured raw; the runtime
6884                        // resolves + validates it as a `u32`.
6885                        "limit" => {
6886                            let t = self.current().clone();
6887                            match t.ttype {
6888                                TokenType::Integer | TokenType::StringLit => {
6889                                    limit_expr = t.value.clone();
6890                                    self.advance();
6891                                }
6892                                _ => self.skip_value(),
6893                            }
6894                        }
6895                        // v2.33.0 — `aggregate:` is a string literal from
6896                        // the CLOSED catalog (`"count"`, `"sum(tokens)"`, …);
6897                        // `group_by:` is a string literal listing columns
6898                        // (`"industry, status"`). Both captured raw; the
6899                        // v1.31.0 proof (axon-T843/T844/T845) + the runtime
6900                        // (`filter::parse_aggregate_clause`) validate.
6901                        "aggregate" => {
6902                            aggregate = self.consume(TokenType::StringLit)?.value.clone()
6903                        }
6904                        "group_by" => {
6905                            group_by = self.consume(TokenType::StringLit)?.value.clone()
6906                        }
6907                        // v2.40.0 — `cache:` names a declared `cache`
6908                        // policy. A retrieve reads a store (never `pure`), so
6909                        // caching it always accepts staleness — the checker
6910                        // requires a finite `ttl:` on the referenced cache
6911                        // (axon-T865) and resolves the reference (axon-T864).
6912                        "cache" => cache = self.consume_any_ident_or_kw()?.value.clone(),
6913                        _ => self.skip_value(),
6914                    }
6915                }
6916            }
6917            if self.check(TokenType::RBrace) {
6918                self.advance();
6919            }
6920        }
6921        Ok(FlowStep::Retrieve(RetrieveStep {
6922            store_name: store,
6923            where_expr,
6924            alias,
6925            order_by,
6926            limit_expr,
6927            aggregate,
6928            group_by,
6929            cache,
6930            loc: Loc {
6931                line: tok.line,
6932                column: tok.column,
6933            },
6934        }))
6935    }
6936
6937    /// v1.30.0 — Parse a `purge` step, capturing the optional
6938    /// `{ where: "<expr>" }` filter. (v1.30.0 moved `mutate` to its
6939    /// own `parse_mutate_step`, which also captures SET columns; this
6940    /// helper now serves `purge` alone — a `DELETE` has no SET clause.)
6941    ///
6942    /// Before v1.30.0 these two steps parsed via `parse_flow_step_simple`,
6943    /// which *skipped* the braced block — so a written `where:` clause
6944    /// was silently dropped and every `mutate`/`purge` ran against the
6945    /// whole store, leaving the entire v1.30.0 parameterized-filter
6946    /// machinery unreachable for them. This mirror of `parse_retrieve_step`
6947    /// (minus the `as:` alias — a mutate/purge binds no result) closes
6948    /// that gap. Returns `(loc, store_name, where_expr)`.
6949    fn parse_store_where_step(
6950        &mut self,
6951    ) -> Result<(Loc, String, String), ParseError> {
6952        let tok = self.current().clone();
6953        self.advance(); // consume the keyword
6954        let store = if self.at_declaration_start()
6955            || self.check(TokenType::RBrace)
6956            || self.check(TokenType::Eof)
6957        {
6958            String::new()
6959        } else {
6960            self.consume_any_ident_or_kw()?.value.clone()
6961        };
6962        let mut where_expr = String::new();
6963        if self.check(TokenType::LBrace) {
6964            self.advance();
6965            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6966                let field = self.current().value.clone();
6967                self.advance();
6968                if self.check(TokenType::Colon) {
6969                    self.advance();
6970                    match field.as_str() {
6971                        "where" => {
6972                            where_expr =
6973                                self.consume(TokenType::StringLit)?.value.clone()
6974                        }
6975                        _ => self.skip_value(),
6976                    }
6977                }
6978            }
6979            if self.check(TokenType::RBrace) {
6980                self.advance();
6981            }
6982        }
6983        Ok((
6984            Loc {
6985                line: tok.line,
6986                column: tok.column,
6987            },
6988            store,
6989            where_expr,
6990        ))
6991    }
6992
6993    /// v1.30.0 — Parse a `persist` step, capturing the optional
6994    /// `{ col: value }` field block.
6995    ///
6996    /// Before v1.30.0 `persist` parsed via `parse_flow_step_simple`,
6997    /// which *skipped* the braced block — so a written field block was
6998    /// silently dropped and the runtime fell back to writing every
6999    /// context binding as a row, which fails against any real table
7000    /// (flows always carry more bindings than a table has columns).
7001    /// This captures the declared columns into `PersistStep.fields`;
7002    /// the runtime writes exactly those (interpolated). A `persist`
7003    /// with no block keeps the v1.30.0 user-bindings fallback — fully
7004    /// backward-compatible. Mirror of `parse_retrieve_step`, but the
7005    /// keys are arbitrary column names rather than the fixed
7006    /// `where:` / `as:` filter keys.
7007    ///
7008    /// The optional `into` connector (`persist into <store>`) is
7009    /// accepted and skipped — before v1.30.0 `into` was captured as
7010    /// the store name.
7011    fn parse_persist_step(&mut self) -> Result<FlowStep, ParseError> {
7012        let tok = self.current().clone();
7013        self.advance(); // consume `persist`
7014        // Optional `into` connector — skip it so the store name that
7015        // follows is not mistaken for the target.
7016        if self.current().value == "into" && !self.check(TokenType::LBrace) {
7017            self.advance();
7018        }
7019        let store = if self.at_declaration_start()
7020            || self.check(TokenType::LBrace)
7021            || self.check(TokenType::RBrace)
7022            || self.check(TokenType::Eof)
7023        {
7024            String::new()
7025        } else {
7026            self.consume_any_ident_or_kw()?.value.clone()
7027        };
7028        let mut fields: Vec<(String, String)> = Vec::new();
7029        if self.check(TokenType::LBrace) {
7030            self.advance();
7031            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7032                let col = self.current().value.clone();
7033                self.advance();
7034                if self.check(TokenType::Colon) {
7035                    self.advance();
7036                    let value = if self.check(TokenType::StringLit) {
7037                        self.consume(TokenType::StringLit)?.value.clone()
7038                    } else if self.check(TokenType::RBrace)
7039                        || self.check(TokenType::Eof)
7040                        || self.check(TokenType::Colon)
7041                    {
7042                        String::new()
7043                    } else {
7044                        let v = self.current().clone();
7045                        self.advance();
7046                        v.value.clone()
7047                    };
7048                    fields.push((col, value));
7049                }
7050            }
7051            if self.check(TokenType::RBrace) {
7052                self.advance();
7053            }
7054        }
7055        Ok(FlowStep::Persist(PersistStep {
7056            store_name: store,
7057            fields,
7058            loc: Loc {
7059                line: tok.line,
7060                column: tok.column,
7061            },
7062        }))
7063    }
7064
7065    /// v1.30.0 — Parse a `mutate` step, capturing both the
7066    /// `{ where: "<expr>" }` filter AND the `{ col: value }` SET
7067    /// assignments.
7068    ///
7069    /// Before v1.30.0 `mutate` parsed via `parse_store_where_step`,
7070    /// which captured only `where:` and *skipped* every other key — so
7071    /// the runtime built the `UPDATE … SET` clause from every flow
7072    /// binding (params + step results + `let`s), which fails against
7073    /// any real table (`column "X" does not exist`). This closes the
7074    /// gap symmetrically to 35.o's `persist` block: every key other
7075    /// than `where:` is a SET column; a `mutate` with no SET column
7076    /// keeps the v1.31.0 user-bindings fallback. `where:` keeps its
7077    /// string-literal grammar (as in `retrieve` / `purge`).
7078    fn parse_mutate_step(&mut self) -> Result<FlowStep, ParseError> {
7079        let tok = self.current().clone();
7080        self.advance(); // consume `mutate`
7081        let store = if self.at_declaration_start()
7082            || self.check(TokenType::LBrace)
7083            || self.check(TokenType::RBrace)
7084            || self.check(TokenType::Eof)
7085        {
7086            String::new()
7087        } else {
7088            self.consume_any_ident_or_kw()?.value.clone()
7089        };
7090        let mut where_expr = String::new();
7091        let mut fields: Vec<(String, String)> = Vec::new();
7092        if self.check(TokenType::LBrace) {
7093            self.advance();
7094            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7095                let key = self.current().value.clone();
7096                self.advance();
7097                if self.check(TokenType::Colon) {
7098                    self.advance();
7099                    if key == "where" {
7100                        where_expr =
7101                            self.consume(TokenType::StringLit)?.value.clone();
7102                    } else {
7103                        let value = if self.check(TokenType::StringLit) {
7104                            self.consume(TokenType::StringLit)?.value.clone()
7105                        } else if self.check(TokenType::RBrace)
7106                            || self.check(TokenType::Eof)
7107                            || self.check(TokenType::Colon)
7108                        {
7109                            String::new()
7110                        } else {
7111                            let v = self.current().clone();
7112                            self.advance();
7113                            v.value.clone()
7114                        };
7115                        fields.push((key, value));
7116                    }
7117                }
7118            }
7119            if self.check(TokenType::RBrace) {
7120                self.advance();
7121            }
7122        }
7123        Ok(FlowStep::Mutate(MutateStep {
7124            store_name: store,
7125            where_expr,
7126            fields,
7127            loc: Loc {
7128                line: tok.line,
7129                column: tok.column,
7130            },
7131        }))
7132    }
7133
7134    // ── TIER 2 DECLARATIONS ────────────────────────────────────────
7135
7136    fn parse_agent(&mut self) -> Result<AgentDefinition, ParseError> {
7137        let tok = self.consume(TokenType::Agent)?;
7138        let name = self.consume(TokenType::Identifier)?.value;
7139        let mut node = AgentDefinition {
7140            name,
7141            goal: String::new(),
7142            tools: Vec::new(),
7143            memory_ref: String::new(),
7144            strategy: String::new(),
7145            on_stuck: String::new(),
7146            shield_ref: String::new(),
7147            max_iterations: None,
7148            max_tokens: None,
7149            max_time: String::new(),
7150            max_cost: None,
7151            return_type: String::new(),
7152            body: Vec::new(),
7153            loc: Loc {
7154                line: tok.line,
7155                column: tok.column,
7156            },
7157            leading_trivia: Vec::new(),
7158            trailing_trivia: Vec::new(),
7159        };
7160        // Optional signature position: `agent Name(params…) -> T {`. The
7161        // parameter list is accepted and not modelled (an agent takes its input
7162        // from the call site); the return type IS modelled — it used to be
7163        // skipped here, which is how `return:` became a promise the README made
7164        // and nothing read.
7165        if self.check(TokenType::LParen) {
7166            let mut depth = 0usize;
7167            while !self.check(TokenType::Eof) {
7168                if self.check(TokenType::LParen) {
7169                    depth += 1;
7170                } else if self.check(TokenType::RParen) {
7171                    depth -= 1;
7172                    if depth == 0 {
7173                        self.advance();
7174                        break;
7175                    }
7176                }
7177                self.advance();
7178            }
7179        }
7180        if self.check(TokenType::Arrow) {
7181            self.advance();
7182            node.return_type = self.parse_output_type_string()?;
7183        }
7184        self.consume(TokenType::LBrace)?;
7185        // The block is a CLOSED catalogue. An unknown field used to be skipped
7186        // in silence, so a typo (`max_iteration: 6`) parsed clean and the agent
7187        // ran unbounded until the dispatcher refused it — the opposite of what
7188        // a type error is for. Every field the runtime reads is listed here;
7189        // `step … { … }` blocks form the `custom` policy's body.
7190        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7191            if self.check(TokenType::Step) {
7192                let step = self.parse_step()?;
7193                node.body.push(step);
7194                continue;
7195            }
7196            let field = self.current().clone();
7197            let field_name = field.value.clone();
7198            self.advance();
7199            if !self.check(TokenType::Colon) {
7200                return Err(self.error(&format!(
7201                    "unexpected `{field_name}` inside `agent {}` — an agent block holds \
7202                     `field: value` pairs and `step Name {{ … }}` blocks; valid fields: \
7203                     goal, tools, memory, strategy, on_stuck, shield, max_iterations, \
7204                     max_tokens, max_time, max_cost, return",
7205                    node.name
7206                )));
7207            }
7208            self.advance();
7209            match field_name.as_str() {
7210                "goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
7211                "tools" => node.tools = self.parse_bracketed_identifiers()?,
7212                "memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
7213                "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
7214                "on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
7215                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
7216                "max_iterations" => node.max_iterations = self.parse_optional_int(),
7217                "max_tokens" => node.max_tokens = self.parse_optional_int(),
7218                "max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
7219                "max_cost" => node.max_cost = self.parse_optional_float(),
7220                "return" => node.return_type = self.parse_output_type_string()?,
7221                other => {
7222                    return Err(self.error(&format!(
7223                        "unknown agent field `{other}` in `agent {}` — the agent block is a \
7224                         closed catalog; valid fields: goal, tools, memory, strategy, \
7225                         on_stuck, shield, max_iterations, max_tokens, max_time, max_cost, \
7226                         return (plus `step Name {{ … }}` blocks for `strategy: custom`)",
7227                        node.name
7228                    )));
7229                }
7230            }
7231        }
7232        self.consume(TokenType::RBrace)?;
7233        Ok(node)
7234    }
7235
7236    /// v2.5.0 — `extension Name { category: effects|scan, members: [ … ] }`.
7237    /// The parser is permissive on field/category VALUES (validated in
7238    /// v2.5.0 by the type-checker — no-shadowing, category-membership);
7239    /// it only enforces the structural grammar here.
7240    fn parse_extension(&mut self) -> Result<ExtensionDefinition, ParseError> {
7241        let tok = self.consume(TokenType::Extension)?;
7242        let name = self.consume(TokenType::Identifier)?.value;
7243        let mut node = ExtensionDefinition {
7244            name,
7245            category: String::new(),
7246            members: Vec::new(),
7247            loc: Loc {
7248                line: tok.line,
7249                column: tok.column,
7250            },
7251            leading_trivia: Vec::new(),
7252            trailing_trivia: Vec::new(),
7253        };
7254        self.consume(TokenType::LBrace)?;
7255        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7256            let field_name = self.current().value.clone();
7257            self.advance();
7258            if self.check(TokenType::Colon) {
7259                self.advance();
7260                match field_name.as_str() {
7261                    "category" => {
7262                        node.category = self.consume_any_ident_or_kw()?.value.clone()
7263                    }
7264                    "members" => node.members = self.parse_extension_members()?,
7265                    _ => self.skip_value(),
7266                }
7267            } else if self.check(TokenType::LBrace) {
7268                self.skip_braced_block()?;
7269            }
7270        }
7271        self.consume(TokenType::RBrace)?;
7272        Ok(node)
7273    }
7274
7275    /// v2.5.0 — parse `[ "name" [: { semantics: "…", default_confidence: 0.8 } ], … ]`.
7276    /// Each member is a string literal optionally followed by a metadata
7277    /// block. Trailing/interleaved commas are tolerated.
7278    fn parse_extension_members(&mut self) -> Result<Vec<ExtensionMember>, ParseError> {
7279        let mut members = Vec::new();
7280        self.consume(TokenType::LBracket)?;
7281        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
7282            let name_tok = self.consume(TokenType::StringLit)?;
7283            let mut member = ExtensionMember {
7284                name: name_tok.value.clone(),
7285                semantics: None,
7286                default_confidence: None,
7287                loc: Loc {
7288                    line: name_tok.line,
7289                    column: name_tok.column,
7290                },
7291            };
7292            // Optional `: { semantics: "…", default_confidence: 0.8 }`.
7293            if self.check(TokenType::Colon) {
7294                self.advance();
7295                self.consume(TokenType::LBrace)?;
7296                while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7297                    let mkey = self.current().value.clone();
7298                    self.advance();
7299                    if self.check(TokenType::Colon) {
7300                        self.advance();
7301                        match mkey.as_str() {
7302                            "semantics" => {
7303                                member.semantics =
7304                                    Some(self.consume(TokenType::StringLit)?.value.clone())
7305                            }
7306                            "default_confidence" => {
7307                                member.default_confidence = self.parse_optional_float()
7308                            }
7309                            _ => self.skip_value(),
7310                        }
7311                    }
7312                    if self.check(TokenType::Comma) {
7313                        self.advance();
7314                    }
7315                }
7316                self.consume(TokenType::RBrace)?;
7317            }
7318            members.push(member);
7319            if self.check(TokenType::Comma) {
7320                self.advance();
7321            }
7322        }
7323        self.consume(TokenType::RBracket)?;
7324        Ok(members)
7325    }
7326
7327    /// v2.27.0 — `window <Name> { timezone: "…" allow: [ {days hours} ]
7328    /// exclude: [ "YYYY-MM-DD", … ]  on_outside: skip|defer|warn }`.
7329    fn parse_window(&mut self) -> Result<WindowDefinition, ParseError> {
7330        let tok = self.consume(TokenType::Window)?;
7331        let name = self.consume(TokenType::Identifier)?.value;
7332        let mut node = WindowDefinition {
7333            name,
7334            timezone: String::new(),
7335            allow: Vec::new(),
7336            exclude: Vec::new(),
7337            on_outside: String::new(),
7338            loc: Loc {
7339                line: tok.line,
7340                column: tok.column,
7341            },
7342            leading_trivia: Vec::new(),
7343            trailing_trivia: Vec::new(),
7344        };
7345        self.consume(TokenType::LBrace)?;
7346        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7347            let field_name = self.consume_any_ident_or_kw()?.value;
7348            self.consume(TokenType::Colon)?;
7349            match field_name.as_str() {
7350                "timezone" => node.timezone = self.consume(TokenType::StringLit)?.value,
7351                "allow" => node.allow = self.parse_window_allow()?,
7352                "exclude" => node.exclude = self.parse_window_exclude()?,
7353                "on_outside" => node.on_outside = self.consume_any_ident_or_kw()?.value,
7354                _ => self.skip_value(),
7355            }
7356        }
7357        self.consume(TokenType::RBrace)?;
7358        Ok(node)
7359    }
7360
7361    /// v2.27.0 — the `allow: [ { … }, { … } ]` span list.
7362    fn parse_window_allow(&mut self) -> Result<Vec<WindowSpan>, ParseError> {
7363        self.consume(TokenType::LBracket)?;
7364        let mut spans = Vec::new();
7365        if !self.check(TokenType::RBracket) {
7366            spans.push(self.parse_window_span()?);
7367            while self.check(TokenType::Comma) {
7368                self.advance();
7369                if self.check(TokenType::RBracket) {
7370                    break; // trailing comma
7371                }
7372                spans.push(self.parse_window_span()?);
7373            }
7374        }
7375        self.consume(TokenType::RBracket)?;
7376        Ok(spans)
7377    }
7378
7379    /// v2.27.0 — the `exclude: [ "YYYY-MM-DD", … ]` holiday list (ISO
7380    /// date-string literals; validated for real-calendar-date-ness by the
7381    /// `axon-T826` type check). An empty list / absent field ⇒ no holidays.
7382    fn parse_window_exclude(&mut self) -> Result<Vec<String>, ParseError> {
7383        self.consume(TokenType::LBracket)?;
7384        let mut dates = Vec::new();
7385        if !self.check(TokenType::RBracket) {
7386            dates.push(self.consume(TokenType::StringLit)?.value);
7387            while self.check(TokenType::Comma) {
7388                self.advance();
7389                if self.check(TokenType::RBracket) {
7390                    break; // trailing comma
7391                }
7392                dates.push(self.consume(TokenType::StringLit)?.value);
7393            }
7394        }
7395        self.consume(TokenType::RBracket)?;
7396        Ok(dates)
7397    }
7398
7399    /// v2.27.0 — one span `{ days: Mon..Fri hours: 9..18 }`.
7400    fn parse_window_span(&mut self) -> Result<WindowSpan, ParseError> {
7401        let tok = self.consume(TokenType::LBrace)?;
7402        let mut span = WindowSpan {
7403            day_start: String::new(),
7404            day_end: String::new(),
7405            hour_start: 0,
7406            hour_end: 0,
7407            loc: Loc {
7408                line: tok.line,
7409                column: tok.column,
7410            },
7411        };
7412        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7413            let field = self.consume_any_ident_or_kw()?.value;
7414            self.consume(TokenType::Colon)?;
7415            match field.as_str() {
7416                "days" => {
7417                    span.day_start = self.consume_any_ident_or_kw()?.value;
7418                    self.consume(TokenType::DotDot)?;
7419                    span.day_end = self.consume_any_ident_or_kw()?.value;
7420                }
7421                "hours" => {
7422                    span.hour_start = self.consume_number()? as i64;
7423                    self.consume(TokenType::DotDot)?;
7424                    span.hour_end = self.consume_number()? as i64;
7425                }
7426                _ => self.skip_value(),
7427            }
7428            if self.check(TokenType::Comma) {
7429                self.advance();
7430            }
7431        }
7432        self.consume(TokenType::RBrace)?;
7433        Ok(span)
7434    }
7435
7436    fn parse_shield(&mut self) -> Result<ShieldDefinition, ParseError> {
7437        let tok = self.consume(TokenType::Shield)?;
7438        let name = self.consume(TokenType::Identifier)?.value;
7439        let mut node = ShieldDefinition {
7440            name,
7441            scan: Vec::new(),
7442            strategy: String::new(),
7443            on_breach: String::new(),
7444            severity: String::new(),
7445            quarantine: String::new(),
7446            max_retries: None,
7447            confidence_threshold: None,
7448            allow_tools: Vec::new(),
7449            deny_tools: Vec::new(),
7450            sandbox: None,
7451            redact: Vec::new(),
7452            log: String::new(),
7453            deflect_message: String::new(),
7454            taint: String::new(),
7455            compliance: Vec::new(),
7456            sign: String::new(),
7457            unknown_fields: Vec::new(),
7458            loc: Loc {
7459                line: tok.line,
7460                column: tok.column,
7461            },
7462            leading_trivia: Vec::new(),
7463            trailing_trivia: Vec::new(),
7464        };
7465        self.consume(TokenType::LBrace)?;
7466        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7467            let field_name = self.current().value.clone();
7468            let field_loc = Loc {
7469                line: self.current().line,
7470                column: self.current().column,
7471            };
7472            self.advance();
7473            if self.check(TokenType::Colon) {
7474                self.advance();
7475                match field_name.as_str() {
7476                    "scan" => node.scan = self.parse_bracketed_identifiers()?,
7477                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
7478                    "on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
7479                    "severity" => node.severity = self.consume_any_ident_or_kw()?.value.clone(),
7480                    "quarantine" => {
7481                        node.quarantine = self.consume(TokenType::StringLit)?.value.clone()
7482                    }
7483                    "max_retries" => node.max_retries = self.parse_optional_int(),
7484                    "confidence_threshold" => {
7485                        node.confidence_threshold = self.parse_optional_float()
7486                    }
7487                    "allow_tools" => node.allow_tools = self.parse_bracketed_identifiers()?,
7488                    "deny_tools" => node.deny_tools = self.parse_bracketed_identifiers()?,
7489                    "sandbox" => {
7490                        node.sandbox = Some(self.consume_any_ident_or_kw()?.value == "true")
7491                    }
7492                    "redact" => node.redact = self.parse_bracketed_identifiers()?,
7493                    "log" => node.log = self.consume_any_ident_or_kw()?.value.clone(),
7494                    "deflect_message" => {
7495                        node.deflect_message = self.consume(TokenType::StringLit)?.value.clone()
7496                    }
7497                    "taint" => node.taint = self.consume_any_ident_or_kw()?.value.clone(),
7498                    // ESK — covered regulatory classes.
7499                    "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
7500                    // v2.34.0 — egress signing algorithm (closed catalog,
7501                    // validated by the checker: `axon-T846`).
7502                    "sign" => node.sign = self.consume_any_ident_or_kw()?.value.clone(),
7503                    // v2.34.0 — the value is still skipped (leniency
7504                    // preserved) but the NAME is recorded so the checker
7505                    // emits `axon-W010` instead of a silent drop.
7506                    _ => {
7507                        node.unknown_fields.push((field_name.clone(), field_loc));
7508                        self.skip_value()
7509                    }
7510                }
7511            } else if self.check(TokenType::LBrace) {
7512                self.skip_braced_block()?;
7513            }
7514        }
7515        self.consume(TokenType::RBrace)?;
7516        Ok(node)
7517    }
7518
7519    fn parse_pix(&mut self) -> Result<PixDefinition, ParseError> {
7520        let tok = self.consume(TokenType::Pix)?;
7521        let name = self.consume(TokenType::Identifier)?.value;
7522        let mut node = PixDefinition {
7523            name,
7524            source: String::new(),
7525            depth: None,
7526            branching: None,
7527            model: String::new(),
7528            loc: Loc {
7529                line: tok.line,
7530                column: tok.column,
7531            },
7532            leading_trivia: Vec::new(),
7533            trailing_trivia: Vec::new(),
7534        };
7535        self.consume(TokenType::LBrace)?;
7536        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7537            let field_name = self.current().value.clone();
7538            self.advance();
7539            if self.check(TokenType::Colon) {
7540                self.advance();
7541                match field_name.as_str() {
7542                    "source" => node.source = self.consume(TokenType::StringLit)?.value.clone(),
7543                    "depth" => node.depth = self.parse_optional_int(),
7544                    "branching" => node.branching = self.parse_optional_int(),
7545                    "model" => node.model = self.consume_any_ident_or_kw()?.value.clone(),
7546                    _ => self.skip_value(),
7547                }
7548            } else if self.check(TokenType::LBrace) {
7549                self.skip_braced_block()?;
7550            }
7551        }
7552        self.consume(TokenType::RBrace)?;
7553        Ok(node)
7554    }
7555
7556    /// v2.12.0 — `ledger <Name> { source, depth, branching, model }`.
7557    /// The append-only audit chain (formerly the Provenance-Index reading of
7558    /// `pix`). Field grammar mirrors `pix` (same shape) but the SEMANTICS are
7559    /// audit, not navigation: `depth` = chain retention, `branching` = Merkle
7560    /// factor, `model` = hash slug (sha256 / blake3 / sha3).
7561    fn parse_ledger(&mut self) -> Result<LedgerDefinition, ParseError> {
7562        let tok = self.consume(TokenType::Ledger)?;
7563        let name = self.consume(TokenType::Identifier)?.value;
7564        let mut node = LedgerDefinition {
7565            name,
7566            source: String::new(),
7567            depth: None,
7568            branching: None,
7569            model: String::new(),
7570            loc: Loc {
7571                line: tok.line,
7572                column: tok.column,
7573            },
7574            leading_trivia: Vec::new(),
7575            trailing_trivia: Vec::new(),
7576        };
7577        self.consume(TokenType::LBrace)?;
7578        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7579            let field_name = self.current().value.clone();
7580            self.advance();
7581            if self.check(TokenType::Colon) {
7582                self.advance();
7583                match field_name.as_str() {
7584                    "source" => node.source = self.consume(TokenType::StringLit)?.value.clone(),
7585                    "depth" => node.depth = self.parse_optional_int(),
7586                    "branching" => node.branching = self.parse_optional_int(),
7587                    "model" => node.model = self.consume_any_ident_or_kw()?.value.clone(),
7588                    _ => self.skip_value(),
7589                }
7590            } else if self.check(TokenType::LBrace) {
7591                self.skip_braced_block()?;
7592            }
7593        }
7594        self.consume(TokenType::RBrace)?;
7595        Ok(node)
7596    }
7597
7598    fn parse_psyche(&mut self) -> Result<PsycheDefinition, ParseError> {
7599        let tok = self.consume(TokenType::Psyche)?;
7600        let name = self.consume(TokenType::Identifier)?.value;
7601        let mut node = PsycheDefinition {
7602            name,
7603            dimensions: Vec::new(),
7604            manifold_noise: None,
7605            manifold_momentum: None,
7606            safety_constraints: Vec::new(),
7607            quantum_enabled: None,
7608            inference_mode: String::new(),
7609            loc: Loc {
7610                line: tok.line,
7611                column: tok.column,
7612            },
7613            leading_trivia: Vec::new(),
7614            trailing_trivia: Vec::new(),
7615        };
7616        self.consume(TokenType::LBrace)?;
7617        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7618            let field_name = self.current().value.clone();
7619            self.advance();
7620            if self.check(TokenType::Colon) {
7621                self.advance();
7622                match field_name.as_str() {
7623                    "dimensions" => node.dimensions = self.parse_bracketed_identifiers()?,
7624                    "manifold_noise" => node.manifold_noise = self.parse_optional_float(),
7625                    "manifold_momentum" => node.manifold_momentum = self.parse_optional_float(),
7626                    // v2.83.0 — `safety:` is what README psyche publishes;
7627                    // `safety_constraints:` is what the parser has always taken.
7628                    // One field, two spellings — the `epsilon`/`tolerance`
7629                    // resolution of v2.83.0.
7630                    "safety_constraints" | "safety" => {
7631                        node.safety_constraints = self.parse_bracketed_identifiers()?
7632                    }
7633                    "quantum_enabled" => {
7634                        node.quantum_enabled = Some(self.consume_any_ident_or_kw()?.value == "true")
7635                    }
7636                    "inference_mode" => {
7637                        node.inference_mode = self.consume_any_ident_or_kw()?.value.clone()
7638                    }
7639                    _ => self.skip_value(),
7640                }
7641            } else if self.check(TokenType::LBrace) {
7642                self.skip_braced_block()?;
7643            }
7644        }
7645        self.consume(TokenType::RBrace)?;
7646        Ok(node)
7647    }
7648
7649    fn parse_corpus(&mut self) -> Result<CorpusDefinition, ParseError> {
7650        let tok = self.consume(TokenType::Corpus)?;
7651        let name = self.consume(TokenType::Identifier)?.value;
7652        let mut node = CorpusDefinition {
7653            name,
7654            documents: Vec::new(),
7655            relations: Vec::new(),
7656            adaptive: false,
7657            mcp_server: String::new(),
7658            mcp_resource_uri: String::new(),
7659            store_source: None,
7660            loc: Loc {
7661                line: tok.line,
7662                column: tok.column,
7663            },
7664            leading_trivia: Vec::new(),
7665            trailing_trivia: Vec::new(),
7666        };
7667        // corpus Name from mcp("server", "uri")  — static MCP-bound short form.
7668        // corpus Name from axonstore { documents: S(id,title)  relations: … }  —
7669        // v2.14.0 dynamic store-sourced MDN graph (falls through to the body).
7670        let mut dynamic = false;
7671        if self.check(TokenType::From) {
7672            self.advance();
7673            if self.check(TokenType::AxonStore) {
7674                self.advance();
7675                dynamic = true;
7676            } else {
7677                self.consume(TokenType::Mcp)?;
7678                self.consume(TokenType::LParen)?;
7679                node.mcp_server = self.consume(TokenType::StringLit)?.value.clone();
7680                self.consume(TokenType::Comma)?;
7681                node.mcp_resource_uri = self.consume(TokenType::StringLit)?.value.clone();
7682                self.consume(TokenType::RParen)?;
7683                return Ok(node);
7684            }
7685        }
7686        self.consume(TokenType::LBrace)?;
7687        // v2.14.0 — accumulate the store-mapping pieces while the dynamic body
7688        // is parsed; folded into `node.store_source` after the closing brace.
7689        let mut src = CorpusStoreSource {
7690            doc_store: String::new(),
7691            doc_id_col: String::new(),
7692            doc_title_col: String::new(),
7693            edge_store: String::new(),
7694            edge_from_col: String::new(),
7695            edge_to_col: String::new(),
7696            edge_type_col: String::new(),
7697            edge_weight_col: String::new(),
7698            loc: Loc {
7699                line: tok.line,
7700                column: tok.column,
7701            },
7702        };
7703        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7704            let field_name = self.current().value.clone();
7705            self.advance();
7706            if self.check(TokenType::Colon) {
7707                self.advance();
7708                match field_name.as_str() {
7709                    // v2.14.0 — dynamic: `documents: <DocStore>(id_col, title_col)`.
7710                    "documents" if dynamic => {
7711                        let (store, cols) = self.parse_corpus_store_mapping(2)?;
7712                        src.doc_store = store;
7713                        src.doc_id_col = cols[0].clone();
7714                        src.doc_title_col = cols[1].clone();
7715                    }
7716                    "documents" => node.documents = self.parse_bracketed_identifiers()?,
7717                    // v2.14.0 — dynamic: `relations: <EdgeStore>(from, to, etype, weight)`.
7718                    "relations" if dynamic => {
7719                        let (store, cols) = self.parse_corpus_store_mapping(4)?;
7720                        src.edge_store = store;
7721                        src.edge_from_col = cols[0].clone();
7722                        src.edge_to_col = cols[1].clone();
7723                        src.edge_type_col = cols[2].clone();
7724                        src.edge_weight_col = cols[3].clone();
7725                    }
7726                    // v2.13.0 — static typed weighted edges → MDN corpus graph.
7727                    "relations" => node.relations = self.parse_corpus_relations()?,
7728                    // v2.13.0 — enable the memory endofunctor.
7729                    "adaptive" => node.adaptive = self.consume_any_ident_or_kw()?.value == "true",
7730                    _ => self.skip_value(),
7731                }
7732            } else if self.check(TokenType::LBrace) {
7733                self.skip_braced_block()?;
7734            }
7735        }
7736        self.consume(TokenType::RBrace)?;
7737        if dynamic {
7738            node.store_source = Some(src);
7739        }
7740        Ok(node)
7741    }
7742
7743    /// v2.14.0 — parse a store-mapping `<StoreName>(col1, col2, …)` of exactly
7744    /// `n` columns. Used by the dynamic store-sourced corpus's `documents:` (2
7745    /// cols: id, title) and `relations:` (4 cols: from, to, etype, weight). The
7746    /// store name is an identifier (a declared `axonstore`); the columns may be
7747    /// keywords (a column could be named `from`/`type`), so they use the
7748    /// keyword-tolerant consumer. The type-checker validates store + columns.
7749    fn parse_corpus_store_mapping(&mut self, n: usize) -> Result<(String, Vec<String>), ParseError> {
7750        let store = self.consume(TokenType::Identifier)?.value.clone();
7751        self.consume(TokenType::LParen)?;
7752        let mut cols = Vec::with_capacity(n);
7753        for i in 0..n {
7754            if i > 0 {
7755                self.consume(TokenType::Comma)?;
7756            }
7757            cols.push(self.consume_any_ident_or_kw()?.value.clone());
7758        }
7759        self.consume(TokenType::RParen)?;
7760        Ok((store, cols))
7761    }
7762
7763    /// v2.13.0 — parse `relations: [ etype(from, to, weight) … ]`, the typed
7764    /// weighted edges of an MDN corpus graph. Entries are whitespace/newline
7765    /// separated; commas between them are optional. Edge-type validity (closed
7766    /// catalog), document references, and the weight range are checked by the
7767    /// type-checker (`check_corpus`), not here.
7768    fn parse_corpus_relations(&mut self) -> Result<Vec<CorpusRelation>, ParseError> {
7769        let mut out = Vec::new();
7770        self.consume(TokenType::LBracket)?;
7771        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
7772            if self.check(TokenType::Comma) {
7773                self.advance();
7774                continue;
7775            }
7776            let tok = self.current().clone();
7777            let etype = self.consume_any_ident_or_kw()?.value.clone();
7778            self.consume(TokenType::LParen)?;
7779            let from = self.consume_any_ident_or_kw()?.value.clone();
7780            self.consume(TokenType::Comma)?;
7781            let to = self.consume_any_ident_or_kw()?.value.clone();
7782            self.consume(TokenType::Comma)?;
7783            let weight = self.consume_number()?;
7784            self.consume(TokenType::RParen)?;
7785            out.push(CorpusRelation {
7786                etype,
7787                from,
7788                to,
7789                weight,
7790                loc: Loc { line: tok.line, column: tok.column },
7791            });
7792        }
7793        self.consume(TokenType::RBracket)?;
7794        Ok(out)
7795    }
7796
7797    /// v2.63.0 — the typed dataspace declaration:
7798    ///
7799    /// ```text
7800    /// dataspace <Name> {
7801    ///     column <name>: <Type>
7802    ///     …
7803    /// }
7804    /// ```
7805    ///
7806    /// Until 108.b the body was consumed by `skip_braced_block()` — any
7807    /// content, including garbage, compiled clean and reached nothing.
7808    /// Now each entry must be a `column` field; the declared type is
7809    /// kept RAW here and resolved against the closed 6-type catalog by
7810    /// the type-checker (`axon-T928`), so all schema errors accumulate
7811    /// in a single compile. An unknown body keyword is a parse error
7812    /// (the grammar is closed — the v1.31.0 axonstore posture).
7813    fn parse_dataspace(&mut self) -> Result<DataspaceDefinition, ParseError> {
7814        let tok = self.consume(TokenType::Dataspace)?;
7815        let name = self.consume(TokenType::Identifier)?.value;
7816        let mut node = DataspaceDefinition {
7817            name,
7818            columns: Vec::new(),
7819            loc: Loc {
7820                line: tok.line,
7821                column: tok.column,
7822            },
7823            leading_trivia: Vec::new(),
7824            trailing_trivia: Vec::new(),
7825        };
7826        if self.check(TokenType::LBrace) {
7827            self.consume(TokenType::LBrace)?;
7828            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7829                let entry = self.current().clone();
7830                if entry.value != "column" {
7831                    return Err(ParseError {
7832                        message: format!(
7833                            "Unknown entry `{}` in dataspace `{}`. A dataspace body \
7834                             declares its columnar schema: `column <name>: <Type>` \
7835                             (one per line, over the closed type catalog — \
7836                             Text, Int, Float, Bool, Timestamp, Json).",
7837                            entry.value, node.name
7838                        ),
7839                        line: entry.line,
7840                        column: entry.column,
7841                        ..Default::default()
7842                    });
7843                }
7844                self.advance(); // `column`
7845                let col_tok = self.current().clone();
7846                let col_name = self.consume_any_ident_or_kw()?.value.clone();
7847                self.consume(TokenType::Colon)?;
7848                let declared_type = self.consume_any_ident_or_kw()?.value.clone();
7849                node.columns.push(crate::ast::DataspaceColumn {
7850                    name: col_name,
7851                    declared_type,
7852                    loc: Loc {
7853                        line: col_tok.line,
7854                        column: col_tok.column,
7855                    },
7856                });
7857            }
7858            self.consume(TokenType::RBrace)?;
7859        }
7860        Ok(node)
7861    }
7862
7863    fn parse_ots(&mut self) -> Result<OtsDefinition, ParseError> {
7864        let tok = self.consume(TokenType::Ots)?;
7865        let name = self.consume(TokenType::Identifier)?.value;
7866        let mut node = OtsDefinition {
7867            name,
7868            teleology: String::new(),
7869            homotopy_search: String::new(),
7870            loss_function: String::new(),
7871            loc: Loc {
7872                line: tok.line,
7873                column: tok.column,
7874            },
7875            leading_trivia: Vec::new(),
7876            trailing_trivia: Vec::new(),
7877        };
7878        // Skip optional type params <In, Out>
7879        if self.check(TokenType::Lt) {
7880            while !self.check(TokenType::Gt) && !self.check(TokenType::Eof) {
7881                self.advance();
7882            }
7883            if self.check(TokenType::Gt) {
7884                self.advance();
7885            }
7886        }
7887        self.consume(TokenType::LBrace)?;
7888        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7889            let field_name = self.current().value.clone();
7890            self.advance();
7891            if self.check(TokenType::Colon) {
7892                self.advance();
7893                match field_name.as_str() {
7894                    "teleology" => {
7895                        node.teleology = self.consume(TokenType::StringLit)?.value.clone()
7896                    }
7897                    "homotopy_search" => {
7898                        node.homotopy_search = self.consume_any_ident_or_kw()?.value.clone()
7899                    }
7900                    // v2.83.0 — README's ots blocks write the loss as a bare
7901                    // identifier (`loss_function: SemanticPreservation`, `L2`,
7902                    // `Contrastive`); the parser accepted only a string literal, so
7903                    // all three published blocks failed at this exact token. Both
7904                    // spellings resolve to the same field.
7905                    "loss_function" => {
7906                        node.loss_function = if self.check(TokenType::StringLit) {
7907                            self.consume(TokenType::StringLit)?.value.clone()
7908                        } else {
7909                            self.consume_any_ident_or_kw()?.value.clone()
7910                        }
7911                    }
7912                    _ => self.skip_value(),
7913                }
7914            } else if self.check(TokenType::LBrace) {
7915                self.skip_braced_block()?;
7916            }
7917        }
7918        self.consume(TokenType::RBrace)?;
7919        Ok(node)
7920    }
7921
7922    fn parse_mandate(&mut self) -> Result<MandateDefinition, ParseError> {
7923        let tok = self.consume(TokenType::Mandate)?;
7924        let name = self.consume(TokenType::Identifier)?.value;
7925        let mut node = MandateDefinition {
7926            name,
7927            constraint: String::new(),
7928            kp: None,
7929            ki: None,
7930            kd: None,
7931            tolerance: None,
7932            max_steps: None,
7933            drift_bound: None,
7934            lipschitz: None,
7935            on_violation: String::new(),
7936            loc: Loc {
7937                line: tok.line,
7938                column: tok.column,
7939            },
7940            leading_trivia: Vec::new(),
7941            trailing_trivia: Vec::new(),
7942        };
7943        self.consume(TokenType::LBrace)?;
7944        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7945            let field_name = self.current().value.clone();
7946            self.advance();
7947            if self.check(TokenType::Colon) {
7948                self.advance();
7949                match field_name.as_str() {
7950                    "constraint" => {
7951                        node.constraint = self.consume(TokenType::StringLit)?.value.clone()
7952                    }
7953                    "kp" | "Kp" => node.kp = self.parse_optional_float(),
7954                    "ki" | "Ki" => node.ki = self.parse_optional_float(),
7955                    "kd" | "Kd" => node.kd = self.parse_optional_float(),
7956                    "max_steps" => node.max_steps = self.parse_optional_int(),
7957                    // v2.83.0 — `epsilon:` is what the README publishes; `tolerance:`
7958                    // is what the parser has always accepted. They are the SAME ε — the
7959                    // convergence band of `Converge(e, ε, N)`. Both spellings resolve here
7960                    // rather than one of them silently vanishing into `skip_value()`.
7961                    "tolerance" | "epsilon" => node.tolerance = self.parse_optional_float(),
7962                    "on_violation" => {
7963                        node.on_violation = self.consume_any_ident_or_kw()?.value.clone()
7964                    }
7965                    _ => self.skip_value(),
7966                }
7967            } else if self.check(TokenType::LBrace) {
7968                // v2.83.0 — `pid { Kp: 2.0, Ki: 0.3, Kd: 0.1 }`, which is the form
7969                // README XV publishes and the form every mandate example uses.
7970                //
7971                // THIS BLOCK USED TO BE `skip_braced_block()`. The consequence was not a
7972                // parse error — it was SILENT ACCEPTANCE: `axon check` printed
7973                // "0 errors" and the IR came out with `kp: None, ki: None, kd: None`.
7974                // The developer wrote the published example, the compiler agreed, and the
7975                // ENTIRE CONTROL LAW was discarded between them. A dropped specification
7976                // that reports success is the v2.67.0 defect living in the parser.
7977                if field_name == "pid" {
7978                    self.parse_pid_block(&mut node)?;
7979                } else if field_name == "stability" {
7980                    self.parse_stability_block(&mut node)?;
7981                } else {
7982                    self.skip_braced_block()?;
7983                }
7984            }
7985        }
7986        self.consume(TokenType::RBrace)?;
7987        Ok(node)
7988    }
7989
7990    /// v2.83.0 — `pid { Kp: <f>, Ki: <f>, Kd: <f> }`.
7991    ///
7992    /// The gains of the Cybernetic Refinement Calculus controller
7993    /// (`papers/paper_mandate.md` section 3): `u(t) = Kp·e(t) + Ki·∫e + Kd·de/dt`.
7994    /// Accepts both capitalised (`Kp`, the papers' and README's notation) and
7995    /// lower-case spellings, because the flat `kp:` form was already accepted and
7996    /// removing it would break programs that use it.
7997    ///
7998    /// v2.83.0 — unknown keys inside the block are REFUSED.
7999    ///
8000    /// v2.83.0 left them skipped, reasoning that the enclosing declaration behaves
8001    /// that way and tightening it was a wider decision. Measuring the published
8002    /// 2.84.0 binary showed what that costs, and the cost is not symmetric:
8003    /// misspelling a GAIN is caught (the missing gain fails the sign conditions),
8004    /// but misspelling a BOUND is not — `stability { drift: 0.5, L: 0.25 }`
8005    /// compiles clean, and the mandate is admitted with no Lyapunov floor at all.
8006    /// The typo does not weaken the check, it DELETES it.
8007    ///
8008    /// These two blocks are not like the enclosing declaration. They are closed
8009    /// catalogues of three and two keys, every one of which is a proof obligation,
8010    /// and an unrecognised key here is never a field a later version will use —
8011    /// it is a typo whose price is a silently discharged safety property.
8012    fn parse_pid_block(&mut self, node: &mut MandateDefinition) -> Result<(), ParseError> {
8013        self.consume(TokenType::LBrace)?;
8014        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8015            let key_token = self.current().clone();
8016            let key = key_token.value.clone();
8017            self.advance();
8018            if self.check(TokenType::Colon) {
8019                self.advance();
8020                match key.as_str() {
8021                    "kp" | "Kp" => node.kp = self.parse_optional_float(),
8022                    "ki" | "Ki" => node.ki = self.parse_optional_float(),
8023                    "kd" | "Kd" => node.kd = self.parse_optional_float(),
8024                    _ => {
8025                        return Err(ParseError {
8026                            message: format!(
8027                                "`{key}` is not a gain of the PID controller. The block accepts \
8028                                 exactly `Kp`, `Ki` and `Kd` (lower-case spellings too). \
8029                                 Skipping what it does not recognise would let a typo drop a \
8030                                 gain, and the stability band is computed from all three."
8031                            ),
8032                            line: key_token.line,
8033                            column: key_token.column,
8034                            ..Default::default()
8035                        });
8036                    }
8037                }
8038            }
8039            if self.check(TokenType::Comma) {
8040                self.advance();
8041            }
8042        }
8043        self.consume(TokenType::RBrace)?;
8044        Ok(())
8045    }
8046
8047    /// v2.83.0 — `stability { D: <f>, L: <f> }`.
8048    ///
8049    /// The declared hypotheses of the mandate's stability theorem: `D` is the
8050    /// drift bound `sup|drift(t)|` (paper_mandate section 3), `L` the Lipschitz
8051    /// constant of the refinement map (prompt_opt section 6.3). With them declared,
8052    /// the type checker verifies the full band `D < |Kp+Ki+Kd| < 1/L`; without
8053    /// them it can verify only the sign conditions, which the papers show to be
8054    /// necessary but not sufficient. The declaration travels in the IR as a
8055    /// proof obligation for dispatch — the compiler never invents these
8056    /// numbers, because they are measured properties of a backend it cannot
8057    /// see, and fabricating them would make the static check vacuous.
8058    ///
8059    /// An empty block is a PARSE error, not a silent no-op: `stability { }`
8060    /// asserts nothing, can discharge nothing, and the developer who wrote it
8061    /// believed otherwise.
8062    fn parse_stability_block(
8063        &mut self,
8064        node: &mut MandateDefinition,
8065    ) -> Result<(), ParseError> {
8066        let open = self.consume(TokenType::LBrace)?;
8067        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8068            let key_token = self.current().clone();
8069            let key = key_token.value.clone();
8070            self.advance();
8071            if self.check(TokenType::Colon) {
8072                self.advance();
8073                match key.as_str() {
8074                    "D" | "d" | "drift_bound" => {
8075                        node.drift_bound = self.parse_optional_float()
8076                    }
8077                    "L" | "l" | "lipschitz" => node.lipschitz = self.parse_optional_float(),
8078                    // v2.83.0 — see `parse_pid_block`. This is the arm that
8079                    // was actually dangerous: a dropped bound is a dropped
8080                    // hypothesis, and the theorem it guards then holds vacuously.
8081                    _ => {
8082                        return Err(ParseError {
8083                            message: format!(
8084                                "`{key}` is not a hypothesis of the stability theorem. The block \
8085                                 accepts exactly `D` (the drift bound, also spelled `d` or \
8086                                 `drift_bound`) and `L` (the Lipschitz constant, also `l` or \
8087                                 `lipschitz`). This is an error rather than a skipped key \
8088                                 because a bound that fails to parse is a bound that is not \
8089                                 declared, and the compiler would then verify the band it can \
8090                                 see — the sign conditions — and admit the mandate as if the \
8091                                 rest had been checked."
8092                            ),
8093                            line: key_token.line,
8094                            column: key_token.column,
8095                            ..Default::default()
8096                        });
8097                    }
8098                }
8099            }
8100            if self.check(TokenType::Comma) {
8101                self.advance();
8102            }
8103        }
8104        self.consume(TokenType::RBrace)?;
8105        if node.drift_bound.is_none() && node.lipschitz.is_none() {
8106            return Err(ParseError {
8107                message: "the `stability { }` block declares neither `D` nor `L` — it                           asserts nothing and can discharge nothing. Declare the drift                           bound (`D:`), the Lipschitz constant (`L:`), or both; or remove                           the block."
8108                    .to_string(),
8109                line: open.line,
8110                column: open.column,
8111                ..Default::default()
8112            });
8113        }
8114        Ok(())
8115    }
8116
8117    /// v2.67.0 — `compute <Name>(p: T, …) -> T { <expr> }`.
8118    ///
8119    /// # What this used to be
8120    ///
8121    /// ```text
8122    /// // Skip optional parameters/return type before brace
8123    /// while !self.check(TokenType::LBrace) { self.advance(); }
8124    /// ```
8125    ///
8126    /// The parameters and the return type were **skipped token by token**, and
8127    /// the brace held only `shield:`. So a `compute` had **no inputs, no output
8128    /// type and no body** — which is why the runtime could do nothing but bind
8129    /// the literal string `"compute:Name(args)"`, and why a downstream step then
8130    /// consumed that text where it expected a number. The README meanwhile
8131    /// promised "native Fast-Path execution bypassing the LLM" **with an O(n)
8132    /// guarantee**.
8133    ///
8134    /// # What it is now
8135    ///
8136    /// A named pure function over the v2.26.0 expression language — the closed,
8137    /// total, side-effect-free term algebra the runtime already evaluates
8138    /// natively (`eval_expr`, the same evaluator behind `let`, `grad` and
8139    /// `conditional`). Linear in the term, no model in the loop: the advertised
8140    /// claim, made true rather than louder.
8141    ///
8142    /// The legacy field form (`compute N { shield: G }`) still parses — its body
8143    /// is simply `None`, and applying a bodyless compute is refused (axon-T941)
8144    /// instead of silently binding a placeholder.
8145    fn parse_compute(&mut self) -> Result<ComputeDefinition, ParseError> {
8146        let tok = self.consume(TokenType::Compute)?;
8147        let name = self.consume(TokenType::Identifier)?.value;
8148        let mut node = ComputeDefinition {
8149            name,
8150            shield_ref: String::new(),
8151            parameters: Vec::new(),
8152            return_type: String::new(),
8153            body: None,
8154            loc: Loc {
8155                line: tok.line,
8156                column: tok.column,
8157            },
8158            leading_trivia: Vec::new(),
8159            trailing_trivia: Vec::new(),
8160        };
8161
8162        // `(p: T, q: T)` — the typed parameters (they used to be skipped).
8163        if self.check(TokenType::LParen) {
8164            self.advance();
8165            while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
8166                let ptok = self.current().clone();
8167                let pname = self.consume_any_ident_or_kw()?.value.clone();
8168                self.consume(TokenType::Colon)?;
8169                let ptype = self.parse_type_expr()?;
8170                node.parameters.push(Parameter {
8171                    name: pname,
8172                    type_expr: ptype,
8173                    loc: self.loc_of(&ptok),
8174                });
8175                if self.check(TokenType::Comma) {
8176                    self.advance();
8177                }
8178            }
8179            self.consume(TokenType::RParen)?;
8180        }
8181
8182        // `-> T` — the declared result type.
8183        if self.check(TokenType::Arrow) {
8184            self.advance();
8185            node.return_type = self.consume_any_ident_or_kw()?.value.clone();
8186        }
8187
8188        self.consume(TokenType::LBrace)?;
8189        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8190            // A `<name>:` pair is a legacy field (only `shield:` is meaningful).
8191            // Anything else is THE BODY — a v2.26.0 expression.
8192            //
8193            // NOTE: the field name may be a KEYWORD, not just an identifier —
8194            // `shield` is `TokenType::Shield`. Testing only for `Identifier` here
8195            // sent `compute N { shield: G }` (the legacy declaration form, and
8196            // the shape of the shipped canonical program) down the
8197            // expression-parsing path and broke it. Back-compat is not optional:
8198            // an adopter's existing program must keep compiling.
8199            let is_field = self
8200                .tokens
8201                .get(self.pos + 1)
8202                .map(|t| t.ttype == TokenType::Colon)
8203                .unwrap_or(false);
8204            if is_field {
8205                let field_tok = self.current().clone();
8206                let field_name = self.current().value.clone();
8207                self.advance();
8208                self.consume(TokenType::Colon)?;
8209                match field_name.as_str() {
8210                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
8211                    // v2.83.0 — `input: a (Float), b (Float)`.
8212                    //
8213                    // This is the parameter list EVERY published compute writes,
8214                    // and it was reaching `skip_value()` — silently discarded, so
8215                    // a compute declared this way had no parameters at all and
8216                    // `run_compute_apply` refused it on arity. The typed form
8217                    // `(a: Float, b: Float)` above stays accepted; both fill the
8218                    // same `parameters`, because they are one concept spelled two
8219                    // ways and a second slot would let them disagree.
8220                    "input" => self.parse_compute_input_list(&mut node)?,
8221                    // v2.83.0 — `output: Float` / `output: PremiumResult`,
8222                    // the field spelling of `-> T`.
8223                    "output" => {
8224                        node.return_type = self.parse_output_type_string()?;
8225                    }
8226                    _ => self.skip_value(),
8227                }
8228                let _ = field_tok;
8229            } else if self.current().value == "logic"
8230                && self
8231                    .tokens
8232                    .get(self.pos + 1)
8233                    .is_some_and(|t| t.ttype == TokenType::LBrace)
8234            {
8235                // v2.83.0 — `logic { let … return … }`, the body form all
8236                // four published computes write. It used to fall to
8237                // `parse_expr()`, which met the bare word `logic` and produced a
8238                // diagnostic about an expression the author never wrote.
8239                if node.body.is_some() {
8240                    return Err(ParseError {
8241                        message: "compute declares two bodies; a pure function has one result, \
8242                                  and keeping the last silently would discard the first"
8243                            .to_string(),
8244                        line: self.current().line,
8245                        column: self.current().column,
8246                        ..Default::default()
8247                    });
8248                }
8249                node.body = Some(self.parse_logic_block()?);
8250            } else {
8251                node.body = Some(self.parse_expr()?);
8252            }
8253        }
8254        self.consume(TokenType::RBrace)?;
8255        Ok(node)
8256    }
8257
8258    /// v2.83.0 — `input: base_rate (Float), risk_factor (Float)`.
8259    ///
8260    /// The published spelling inverts the typed form's punctuation: the name
8261    /// comes first and the type rides in parentheses. Both land in
8262    /// `ComputeDefinition::parameters`.
8263    fn parse_compute_input_list(&mut self, node: &mut ComputeDefinition) -> Result<(), ParseError> {
8264        loop {
8265            let ptok = self.current().clone();
8266            let pname = self.consume_any_ident_or_kw()?.value.clone();
8267            // The type is optional in principle; every published compute writes
8268            // it, and a parameter with no declared type cannot be checked, so an
8269            // absent one is recorded as empty rather than invented.
8270            let type_expr = if self.check(TokenType::LParen) {
8271                self.advance();
8272                let t = self.parse_type_expr()?;
8273                self.consume(TokenType::RParen)?;
8274                t
8275            } else {
8276                TypeExpr {
8277                    name: String::new(),
8278                    generic_param: String::new(),
8279                    optional: false,
8280                    loc: self.loc_of(&ptok),
8281                }
8282            };
8283            node.parameters.push(Parameter {
8284                name: pname,
8285                type_expr,
8286                loc: self.loc_of(&ptok),
8287            });
8288            if self.check(TokenType::Comma) {
8289                self.advance();
8290            } else {
8291                break;
8292            }
8293        }
8294        Ok(())
8295    }
8296
8297    /// v2.83.0 — the `logic { }` body: a chain of `let`s closed by `return`.
8298    ///
8299    /// Lowered to nested [`Expr::Let`] terms, innermost-last, so
8300    /// `let a = e₁  let b = e₂  return e₃` becomes `Let(a, e₁, Let(b, e₂, e₃))`.
8301    /// That is one evaluation per binding — substituting the bindings into the
8302    /// return expression instead would re-evaluate every bound term once per
8303    /// mention.
8304    ///
8305    /// `return` is REQUIRED. A `logic` block whose last statement is a `let`
8306    /// binds names and produces nothing; the compute would then have to invent a
8307    /// result, and inventing the result of a deterministic function is the one
8308    /// thing this primitive exists not to do.
8309    fn parse_logic_block(&mut self) -> Result<Expr, ParseError> {
8310        let open = self.current().clone();
8311        self.advance(); // `logic`
8312        self.consume(TokenType::LBrace)?;
8313
8314        let mut bindings: Vec<(String, Expr)> = Vec::new();
8315        let mut result: Option<Expr> = None;
8316
8317        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8318            if self.check(TokenType::Let) {
8319                if result.is_some() {
8320                    return Err(ParseError {
8321                        message: "a `let` after the `return` in a `logic { }` block is \
8322                                  unreachable — the block's value is already decided. Move it \
8323                                  above the `return`."
8324                            .to_string(),
8325                        line: self.current().line,
8326                        column: self.current().column,
8327                        ..Default::default()
8328                    });
8329                }
8330                self.advance(); // `let`
8331                let name = self.consume_any_ident_or_kw()?.value.clone();
8332                self.consume(TokenType::Assign)?;
8333                bindings.push((name, self.parse_expr()?));
8334            } else if self.check(TokenType::Return) {
8335                self.advance();
8336                result = Some(self.parse_expr()?);
8337            } else {
8338                let bad = self.current().clone();
8339                return Err(ParseError {
8340                    message: format!(
8341                        "unexpected `{}` in a `logic {{ }}` block — it admits only `let <name> = \
8342                         <expr>` bindings and a closing `return <expr>`. `compute` is a PURE \
8343                         function (its own paper: \"pureza categórica de los morfismos \
8344                         funcionales\"), so a statement that could have an effect is refused \
8345                         rather than parsed and dropped.",
8346                        bad.value
8347                    ),
8348                    line: bad.line,
8349                    column: bad.column,
8350                    ..Default::default()
8351                });
8352            }
8353        }
8354        self.consume(TokenType::RBrace)?;
8355
8356        let mut expr = result.ok_or_else(|| ParseError {
8357            message: "a `logic { }` block must end in `return <expr>`. Without it the block binds \
8358                      names and yields nothing, and the compute would have to invent a result — \
8359                      which is precisely what a deterministic primitive must never do."
8360                .to_string(),
8361            line: open.line,
8362            column: open.column,
8363            ..Default::default()
8364        })?;
8365
8366        // Fold innermost-last so the first `let` written is the outermost scope.
8367        for (name, value) in bindings.into_iter().rev() {
8368            expr = Expr::Let {
8369                name,
8370                value: Box::new(value),
8371                body: Box::new(expr),
8372            };
8373        }
8374        Ok(expr)
8375    }
8376
8377    fn parse_daemon(&mut self) -> Result<DaemonDefinition, ParseError> {
8378        let tok = self.consume(TokenType::Daemon)?;
8379        let name = self.consume(TokenType::Identifier)?.value;
8380        let mut node = DaemonDefinition {
8381            name,
8382            goal: String::new(),
8383            tools: Vec::new(),
8384            memory_ref: String::new(),
8385            strategy: String::new(),
8386            on_stuck: String::new(),
8387            shield_ref: String::new(),
8388            window_ref: String::new(),
8389            budget: None,
8390            max_tokens: None,
8391            max_time: String::new(),
8392            max_cost: None,
8393            listeners: Vec::new(),
8394            requires_capabilities: Vec::new(),
8395            loc: Loc {
8396                line: tok.line,
8397                column: tok.column,
8398            },
8399            leading_trivia: Vec::new(),
8400            trailing_trivia: Vec::new(),
8401        };
8402        // Skip optional parameters/return type before brace
8403        while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
8404            self.advance();
8405        }
8406        self.consume(TokenType::LBrace)?;
8407        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8408            let field = self.current().clone();
8409            let field_name = field.value.clone();
8410            self.advance();
8411            if self.check(TokenType::Colon) {
8412                self.advance();
8413                match field_name.as_str() {
8414                    "goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
8415                    "tools" => node.tools = self.parse_bracketed_identifiers()?,
8416                    "memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
8417                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
8418                    "on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
8419                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
8420                    // v2.27.0 — `window: <WindowName>` temporal binding.
8421                    "window" => node.window_ref = self.consume_any_ident_or_kw()?.value.clone(),
8422                    "max_tokens" => node.max_tokens = self.parse_optional_int(),
8423                    "max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
8424                    "max_cost" => node.max_cost = self.parse_optional_float(),
8425                    // v2.4.0 — `requires: [cap, …]` capability scope (same
8426                    // closed slug grammar as `axonendpoint requires:`). The
8427                    // enterprise supervisor mints a per-run principal scoped to
8428                    // exactly these (least privilege).
8429                    "requires" => {
8430                        let bracket_tok = self.current().clone();
8431                        let items = self.parse_bracketed_dot_identifiers()?;
8432                        for slug in &items {
8433                            if !is_valid_capability_slug(slug) {
8434                                return Err(ParseError {
8435                                    message: format!(
8436                                        "Invalid capability slug '{slug}' in daemon '{}' \
8437                                         `requires:`. Capability slugs must match \
8438                                         ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
8439                                         lowercase identifiers. Examples: `daemon.run`, \
8440                                         `memory.write`, `flow.execute`.",
8441                                        node.name
8442                                    ),
8443                                    line: bracket_tok.line,
8444                                    column: bracket_tok.column,
8445                                    ..Default::default()
8446                                });
8447                            }
8448                        }
8449                        node.requires_capabilities = items;
8450                    }
8451                    _ => self.skip_value(),
8452                }
8453            } else if field.ttype == TokenType::Listen {
8454                // v1.6.0 D4 — preserve listen blocks for type
8455                // checking.  We backtracked past the `listen` keyword
8456                // by `advance()` above, so reconstruct a synthetic
8457                // listener using the same dual-mode dispatch the flow
8458                // step parser uses (string topic OR typed channel ref).
8459                let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
8460                    (self.consume(TokenType::StringLit)?.value.clone(), false)
8461                } else {
8462                    (self.consume_any_ident_or_kw()?.value.clone(), true)
8463                };
8464                let mut alias = String::new();
8465                if !self.at_declaration_start()
8466                    && !self.check(TokenType::RBrace)
8467                    && !self.check(TokenType::LBrace)
8468                {
8469                    let next = self.current().clone();
8470                    if next.value == "as" || next.ttype == TokenType::As {
8471                        self.advance();
8472                        alias = self.consume_any_ident_or_kw()?.value.clone();
8473                    }
8474                }
8475                let listen_loc = Loc {
8476                    line: field.line,
8477                    column: field.column,
8478                };
8479                // v2.4.0 — parse the handler body (was skipped). This is
8480                // what makes a `daemon` operational: the body runs per event /
8481                // scheduled tick (e.g. a `listen "cron:…" as tick { run … }`).
8482                let body = self.parse_listener_body()?;
8483                node.listeners.push(ListenStep {
8484                    channel,
8485                    channel_is_ref,
8486                    event_alias: alias,
8487                    body,
8488                    loc: listen_loc,
8489                });
8490            } else if field_name == "budget" && self.check(TokenType::LBrace) {
8491                // v2.28.0 — the `budget { … }` linear-effect rate-limit block.
8492                node.budget = Some(self.parse_budget_block(field.line, field.column)?);
8493            } else if self.check(TokenType::LBrace) {
8494                self.skip_braced_block()?;
8495            }
8496        }
8497        self.consume(TokenType::RBrace)?;
8498        Ok(node)
8499    }
8500
8501    /// v2.69.0 — a TOP-LEVEL `budget <Name> { … }`.
8502    ///
8503    /// Same body as the daemon-attached block; what it gains is a **name** and a
8504    /// **scope that is not a daemon**. Until v2.69.0, `budget` was a field of `daemon`
8505    /// and of nothing else — so an adopter deploying an HTTP endpoint that calls a
8506    /// vendor tool had **no way in the language to bound how often it did that.**
8507    /// Not "the bound did not work": **the bound could not be written.** And the
8508    /// HTTP endpoint is what people actually deploy.
8509    fn parse_top_level_budget(&mut self) -> Result<BudgetBlock, ParseError> {
8510        let kw = self.consume(TokenType::Budget)?; // `budget`
8511        let name = self.consume(TokenType::Identifier)?.value;
8512        let mut block = self.parse_budget_block(kw.line, kw.column)?;
8513        block.name = name;
8514        Ok(block)
8515    }
8516
8517    /// v2.28.0 — `budget { <rate|max>: N per <period> on Tool(<X>) … [on_exhausted: <p>] }`.
8518    fn parse_budget_block(&mut self, line: u32, column: u32) -> Result<BudgetBlock, ParseError> {
8519        self.consume(TokenType::LBrace)?;
8520        let mut quotas = Vec::new();
8521        let mut on_exhausted = String::new();
8522        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8523            let field = self.current().clone();
8524            let field_name = self.consume_any_ident_or_kw()?.value;
8525            match field_name.as_str() {
8526                "rate" | "max" => {
8527                    quotas.push(self.parse_budget_quota(field_name, field.line, field.column)?);
8528                }
8529                "on_exhausted" => {
8530                    self.consume(TokenType::Colon)?;
8531                    on_exhausted = self.consume_any_ident_or_kw()?.value;
8532                }
8533                _ => self.skip_value(),
8534            }
8535        }
8536        self.consume(TokenType::RBrace)?;
8537        Ok(BudgetBlock {
8538            name: String::new(),
8539            quotas,
8540            on_exhausted,
8541            loc: Loc { line, column },
8542            leading_trivia: Vec::new(),
8543            trailing_trivia: Vec::new(),
8544        })
8545    }
8546
8547    /// v2.28.0 — one quota line: `<kind>: <limit> per <period> on Tool(<effect>)`.
8548    /// `kind` (`rate`/`max`) is already consumed by the caller.
8549    fn parse_budget_quota(
8550        &mut self,
8551        kind: String,
8552        line: u32,
8553        column: u32,
8554    ) -> Result<BudgetQuota, ParseError> {
8555        self.consume(TokenType::Colon)?;
8556        let limit = self.consume_number()? as i64;
8557        // `per <period>`
8558        let _per = self.consume_any_ident_or_kw()?; // the `per` keyword
8559        let period = self.consume_any_ident_or_kw()?.value;
8560        // `on Tool(<effect>)`
8561        let _on = self.consume_any_ident_or_kw()?; // the `on` keyword
8562        let _tool = self.consume_any_ident_or_kw()?; // the `Tool` wrapper keyword
8563        self.consume(TokenType::LParen)?;
8564        let effect = self.consume_any_ident_or_kw()?.value;
8565        self.consume(TokenType::RParen)?;
8566        Ok(BudgetQuota {
8567            kind,
8568            limit,
8569            period,
8570            effect,
8571            loc: Loc { line, column },
8572        })
8573    }
8574
8575    fn parse_axonstore(&mut self) -> Result<AxonStoreDefinition, ParseError> {
8576        let tok = self.consume(TokenType::AxonStore)?;
8577        let name = self.consume(TokenType::Identifier)?.value;
8578        let mut node = AxonStoreDefinition {
8579            name,
8580            backend: String::new(),
8581            connection: String::new(),
8582            resource_ref: String::new(),
8583            confidence_floor: None,
8584            isolation: String::new(),
8585            on_breach: String::new(),
8586            capability: String::new(),
8587            class: String::new(),
8588            column_schema: None,
8589            loc: Loc {
8590                line: tok.line,
8591                column: tok.column,
8592            },
8593            leading_trivia: Vec::new(),
8594            trailing_trivia: Vec::new(),
8595        };
8596        self.consume(TokenType::LBrace)?;
8597        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8598            let field = self.current().clone();
8599            let field_name = field.value.clone();
8600            // v1.31.0 (D1) — `schema:` declaration in three closed
8601            // forms: inline column block, manifest reference (string
8602            // literal), or env-var schema namespace (`env:VAR` —
8603            // unquoted or quoted). Parse the form; the v1.31.0 / v1.31.0
8604            // type-checker consumes the resulting AST.
8605            if field.ttype == TokenType::Schema {
8606                self.advance();
8607                let parsed = self.parse_store_schema_declaration(&node.name, field.line, field.column)?;
8608                node.column_schema = Some(parsed);
8609                continue;
8610            }
8611            self.advance();
8612            if self.check(TokenType::Colon) {
8613                self.advance();
8614                match field_name.as_str() {
8615                    "backend" => node.backend = self.consume_any_ident_or_kw()?.value.clone(),
8616                    // v2.48.0 — the secret-class prefix of a
8617                    // `backend: secrets` metadata store. Dotted-identifier
8618                    // form (`class: crm`, `class: crm.oauth`); the
8619                    // secrets-only placement rule + slug shape are
8620                    // `axon-T900` in the type-checker (it needs the
8621                    // resolved `backend:`, which may appear after this
8622                    // field in source order).
8623                    "class" => node.class = self.parse_dotted_identifier()?,
8624                    "connection" => node.connection = self.parse_config_key()?,
8625                    // v2.67.0 — the `resource` this store RUNS ON. When
8626                    // present the store derives its DSN, its POOL SIZE and its
8627                    // sharing discipline from the resource; `connection:`
8628                    // becomes redundant and `axon-T946` refuses declaring both
8629                    // (the same fact, twice, is how the islands happened).
8630                    "resource" => {
8631                        node.resource_ref = self.consume_any_ident_or_kw()?.value.clone()
8632                    }
8633                    "confidence_floor" => node.confidence_floor = self.parse_optional_float(),
8634                    "isolation" => node.isolation = self.consume_any_ident_or_kw()?.value.clone(),
8635                    "on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
8636                    // v1.30.0 (D11) — Pillar IV: the capability slug
8637                    // required to access this store. Validated against
8638                    // the closed slug grammar shared with `requires:`.
8639                    "capability" => {
8640                        let slug_tok = self.consume(TokenType::StringLit)?.clone();
8641                        if !is_valid_capability_slug(&slug_tok.value) {
8642                            return Err(ParseError {
8643                                message: format!(
8644                                    "Invalid capability slug '{}' in axonstore '{}' \
8645                                     `capability:`. Capability slugs must match \
8646                                     ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
8647                                     lowercase identifiers starting with a letter. Examples: \
8648                                     `admin`, `tenant.read`, `hipaa.phi.read`.",
8649                                    slug_tok.value, node.name
8650                                ),
8651                                line: slug_tok.line,
8652                                column: slug_tok.column,
8653                                ..Default::default()
8654                            });
8655                        }
8656                        node.capability = slug_tok.value.clone();
8657                    }
8658                    _ => self.skip_value(),
8659                }
8660            } else if self.check(TokenType::LBrace) {
8661                self.skip_braced_block()?;
8662            }
8663        }
8664        self.consume(TokenType::RBrace)?;
8665        Ok(node)
8666    }
8667
8668    /// v1.31.0 (D1) — parse the three closed forms of an `axonstore`
8669    /// `schema:` declaration:
8670    ///
8671    ///   * form (a) **inline** — `schema { col: Type [constraint…], … }`
8672    ///   * form (b) **manifest reference** — `schema: "qualified.name"`
8673    ///     (string literal that does NOT start with `env:`)
8674    ///   * form (c) **env-var schema namespace** — `schema: env:VAR`
8675    ///     (unquoted) OR `schema: "env:VAR"` (quoted; the literal
8676    ///     starts with `env:`)
8677    ///
8678    /// Called immediately AFTER `schema` is consumed.
8679    fn parse_store_schema_declaration(
8680        &mut self,
8681        store_name: &str,
8682        sch_line: u32,
8683        sch_col: u32,
8684    ) -> Result<crate::store_schema::StoreColumnSchema, ParseError> {
8685        use crate::store_schema::{StoreColumn, StoreColumnSchema, StoreColumnType};
8686
8687        // — Form (a) — inline column block: `schema { ... }`. —
8688        if self.check(TokenType::LBrace) {
8689            self.consume(TokenType::LBrace)?;
8690            let mut columns: Vec<StoreColumn> = Vec::new();
8691            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8692                let col_tok = self.current().clone();
8693                let col_name = self.consume_any_ident_or_kw()?.value.clone();
8694                self.consume(TokenType::Colon)?;
8695                let type_tok = self.consume_any_ident_or_kw()?.clone();
8696                let col_type = StoreColumnType::from_token(&type_tok.value).ok_or_else(|| {
8697                    let names = StoreColumnType::all_canonical_names();
8698                    let suggestion =
8699                        crate::smart_suggest::suggest_for(&type_tok.value, &names);
8700                    let suggest_suffix = if suggestion.is_empty() {
8701                        String::new()
8702                    } else {
8703                        format!(" {suggestion}")
8704                    };
8705                    let known = names.join(", ");
8706                    ParseError {
8707                        message: format!(
8708                            "Unknown column type `{}` for column `{}` in \
8709                             axonstore `{}` `schema:` block. The closed \
8710                             v1.38.0 column-type catalog \
8711                             is {{{known}}} (plus common lowercase \
8712                             aliases — `int`/`integer`/`int4` for \
8713                             `Int`, `bool`/`boolean` for `Bool`, etc.).\
8714                             {suggest_suffix}",
8715                            type_tok.value, col_name, store_name
8716                        ),
8717                        line: type_tok.line,
8718                        column: type_tok.column,
8719                        ..Default::default()
8720                    }
8721                })?;
8722
8723                // v2.26.0 (D1) — the OPTIONAL `Json<T>` shape LENS on a
8724                // column. `payload: Json<UserEvent>` records the expected
8725                // struct shape; the lens is a compile-time expectation only
8726                // (the column stays physically `jsonb`, navigated totally at
8727                // runtime — doctrine `open_data_is_total`). The shape's
8728                // well-formedness (T is a declared `type`) is `axon-T840`
8729                // in the type-checker — it needs the symbol table. Here we
8730                // only enforce the STRUCTURAL rule: a `<T>` lens may refine
8731                // ONLY a `Json` / `Jsonb` column — `axon-T841` otherwise.
8732                let mut json_shape: Option<String> = None;
8733                if self.check(TokenType::Lt) {
8734                    self.advance();
8735                    let shape_tok = self.consume_any_ident_or_kw()?.clone();
8736                    self.consume(TokenType::Gt)?;
8737                    if matches!(col_type, StoreColumnType::Json | StoreColumnType::Jsonb) {
8738                        json_shape = Some(shape_tok.value.clone());
8739                    } else {
8740                        return Err(ParseError {
8741                            message: format!(
8742                                "axon-T841 a shape lens `<{shape}>` may refine \
8743                                 only a `Json` / `Jsonb` column, but column \
8744                                 `{col}` in axonstore `{store}` is `{ty}`. Drop \
8745                                 the `<{shape}>` (a rigid column already has a \
8746                                 fixed shape), or change the column type to \
8747                                 `Json<{shape}>` if it carries open documents.",
8748                                shape = shape_tok.value,
8749                                col = col_name,
8750                                store = store_name,
8751                                ty = col_type.canonical_name(),
8752                            ),
8753                            line: shape_tok.line,
8754                            column: shape_tok.column,
8755                            ..Default::default()
8756                        });
8757                    }
8758                }
8759
8760                let mut col = StoreColumn {
8761                    name: col_name,
8762                    col_type,
8763                    json_shape,
8764                    primary_key: false,
8765                    auto_increment: false,
8766                    not_null: false,
8767                    unique: false,
8768                    indexed: false,
8769                    default_value: String::new(),
8770                    // v1.31.0 (D1) — `identity` is now a recognized
8771                    // inline keyword (see the constraint loop below).
8772                    // Defaults to false; set to true when the adopter
8773                    // writes `id: BigInt primary_key identity`.
8774                    identity: false,
8775                    line: col_tok.line,
8776                    column: col_tok.column,
8777                };
8778
8779                // Trailing constraints (position-independent), matching
8780                // the Python `_parse_store_column` surface.
8781                while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8782                    if self.current().ttype != TokenType::Identifier {
8783                        // The next column starts with a non-identifier
8784                        // (rare) — stop the constraint scan.
8785                        break;
8786                    }
8787                    let constraint = self.current().value.clone();
8788                    match constraint.as_str() {
8789                        "primary_key" => {
8790                            col.primary_key = true;
8791                            self.advance();
8792                        }
8793                        "auto_increment" => {
8794                            col.auto_increment = true;
8795                            self.advance();
8796                        }
8797                        "not_null" => {
8798                            col.not_null = true;
8799                            self.advance();
8800                        }
8801                        "unique" => {
8802                            col.unique = true;
8803                            self.advance();
8804                        }
8805                        // v2.26.0 (D1) — the `index` constraint declares
8806                        // an index as a capability-honest effect (visible to
8807                        // the deploy gate, not a silent DBA action). The
8808                        // backend picks the method from the column type
8809                        // (GIN for a Json/Jsonb column, b-tree otherwise).
8810                        "index" => {
8811                            col.indexed = true;
8812                            self.advance();
8813                        }
8814                        // v1.31.0 (D1) — `identity` marks a column
8815                        // as `GENERATED ALWAYS/BY DEFAULT AS IDENTITY`.
8816                        // Distinct from `auto_increment` (legacy SERIAL
8817                        // via `nextval(...)` default). T803 skips
8818                        // identity columns from the NOT-NULL-omission
8819                        // check because Postgres auto-fills them; the
8820                        // distinction matters because IDENTITY ALWAYS
8821                        // also rejects user-supplied values, where
8822                        // SERIAL accepts them (a future 38.x.e arm in
8823                        // T802 may surface this).
8824                        "identity" => {
8825                            col.identity = true;
8826                            self.advance();
8827                        }
8828                        "default" => {
8829                            self.advance();
8830                            let dv = self.current().clone();
8831                            if matches!(
8832                                dv.ttype,
8833                                TokenType::StringLit
8834                                    | TokenType::Integer
8835                                    | TokenType::Float
8836                            ) {
8837                                col.default_value = dv.value.clone();
8838                                self.advance();
8839                            } else {
8840                                col.default_value =
8841                                    self.consume_any_ident_or_kw()?.value.clone();
8842                            }
8843                        }
8844                        _ => break,
8845                    }
8846                }
8847
8848                columns.push(col);
8849            }
8850            self.consume(TokenType::RBrace)?;
8851            return Ok(StoreColumnSchema::Inline {
8852                columns,
8853                leading_trivia: Vec::new(),
8854                line: sch_line,
8855                column: sch_col,
8856            });
8857        }
8858
8859        // — Forms (b) + (c) require a `:` separator. —
8860        if !self.check(TokenType::Colon) {
8861            let cur = self.current().clone();
8862            return Err(ParseError {
8863                message: format!(
8864                    "axonstore `{store_name}` `schema:` declaration expects \
8865                     `{{ … }}` (inline columns), `: \"manifest.ref\"` \
8866                     (manifest reference), or `: env:VAR` (per-tenant schema \
8867                     namespace). Got `{}` instead.",
8868                    cur.value
8869                ),
8870                line: cur.line,
8871                column: cur.column,
8872                ..Default::default()
8873            });
8874        }
8875        self.consume(TokenType::Colon)?;
8876
8877        // — Form (b) or (c)-quoted — string literal value. —
8878        if self.check(TokenType::StringLit) {
8879            let lit = self.consume(TokenType::StringLit)?.clone();
8880            let value = lit.value.clone();
8881            if let Some(var) = value.strip_prefix("env:") {
8882                let var = var.trim();
8883                if var.is_empty() {
8884                    return Err(ParseError {
8885                        message: format!(
8886                            "axonstore `{store_name}` `schema: \"env:\"` is \
8887                             missing the variable name after the `env:` \
8888                             prefix."
8889                        ),
8890                        line: lit.line,
8891                        column: lit.column,
8892                        ..Default::default()
8893                    });
8894                }
8895                return Ok(StoreColumnSchema::EnvVar {
8896                    var_name: var.to_string(),
8897                    line: sch_line,
8898                    column: sch_col,
8899                });
8900            }
8901            // Plain string → manifest reference.
8902            if value.trim().is_empty() {
8903                return Err(ParseError {
8904                    message: format!(
8905                        "axonstore `{store_name}` `schema:` manifest reference \
8906                         is empty. Expected `\"qualified.name\"` — e.g. \
8907                         `\"public.tenants\"`."
8908                    ),
8909                    line: lit.line,
8910                    column: lit.column,
8911                    ..Default::default()
8912                });
8913            }
8914            return Ok(StoreColumnSchema::ManifestRef {
8915                qualified_name: value,
8916                line: sch_line,
8917                column: sch_col,
8918            });
8919        }
8920
8921        // — Form (c) unquoted — `env:VAR`. The lexer emits `env` as an
8922        //   identifier, then `:`, then the identifier var name. —
8923        let env_tok = self.current().clone();
8924        if env_tok.value == "env" {
8925            self.advance();
8926            if !self.check(TokenType::Colon) {
8927                return Err(ParseError {
8928                    message: format!(
8929                        "axonstore `{store_name}` `schema: env` is missing the \
8930                         `:` separator. Expected `schema: env:VAR`."
8931                    ),
8932                    line: env_tok.line,
8933                    column: env_tok.column,
8934                    ..Default::default()
8935                });
8936            }
8937            self.advance(); // past ':'
8938            let var_tok = self.consume_any_ident_or_kw()?.clone();
8939            if var_tok.value.trim().is_empty() {
8940                return Err(ParseError {
8941                    message: format!(
8942                        "axonstore `{store_name}` `schema: env:` is missing \
8943                         the variable name."
8944                    ),
8945                    line: var_tok.line,
8946                    column: var_tok.column,
8947                    ..Default::default()
8948                });
8949            }
8950            return Ok(StoreColumnSchema::EnvVar {
8951                var_name: var_tok.value.clone(),
8952                line: sch_line,
8953                column: sch_col,
8954            });
8955        }
8956
8957        Err(ParseError {
8958            message: format!(
8959                "axonstore `{store_name}` `schema:` declaration expects \
8960                 `{{ … }}` (inline columns), `\"manifest.ref\"` (manifest \
8961                 reference), or `env:VAR` (per-tenant schema namespace). \
8962                 Got `{}` instead.",
8963                env_tok.value
8964            ),
8965            line: env_tok.line,
8966            column: env_tok.column,
8967            ..Default::default()
8968        })
8969    }
8970
8971    // ── v1.1.0 — Resource primitive ────────────────────────
8972
8973    /// Parse: `resource Name { kind, endpoint, capacity, lifetime, certainty_floor, shield }`.
8974    ///
8975    /// Mirrors `axon.compiler.parser.Parser._parse_resource`. Unknown fields
8976    /// are silently skipped (keeps the grammar forward-compatible).
8977    fn parse_resource(&mut self) -> Result<ResourceDefinition, ParseError> {
8978        let tok = self.consume(TokenType::Resource)?;
8979        let name = self.consume(TokenType::Identifier)?.value;
8980        let mut node = ResourceDefinition {
8981            name,
8982            kind: String::new(),
8983            endpoint: String::new(),
8984            capacity: None,
8985            lifetime: "affine".to_string(),
8986            certainty_floor: None,
8987            shield_ref: String::new(),
8988            within: String::new(),
8989            loc: Loc {
8990                line: tok.line,
8991                column: tok.column,
8992            },
8993            leading_trivia: Vec::new(),
8994            trailing_trivia: Vec::new(),
8995        };
8996        self.consume(TokenType::LBrace)?;
8997        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8998            let field_tok = self.current().clone();
8999            let field_name = field_tok.value.clone();
9000            self.advance();
9001            if !self.check(TokenType::Colon) {
9002                // Tolerate stray brace or unknown layout.
9003                if self.check(TokenType::LBrace) {
9004                    self.skip_braced_block()?;
9005                }
9006                continue;
9007            }
9008            self.advance(); // past ':'
9009            match field_name.as_str() {
9010                "kind" => node.kind = self.consume_any_ident_or_kw()?.value,
9011                // v2.67.0 — `endpoint:` accepts BOTH shapes on purpose:
9012                //   - a dotted config key  (`endpoint: db.main`)      — the law
9013                //   - a string literal     (`endpoint: "postgres://…"`) — the sin
9014                //
9015                // The literal is REFUSED, but by `axon-T944`, not by the parser.
9016                // If it died here the adopter would read "Expected StringLit",
9017                // which explains nothing. The law gets to say why: *URLs and
9018                // credentials never appear in source* — the same sentence
9019                // `axon-T850` has been saying to `upstream.resolve` all along.
9020                //
9021                // A diagnostic that names the rule teaches; one that names the
9022                // token type only tells you the compiler is unhappy.
9023                "endpoint" => {
9024                    node.endpoint = if self.check(TokenType::StringLit) {
9025                        self.consume(TokenType::StringLit)?.value
9026                    } else {
9027                        self.parse_dotted_identifier()?
9028                    };
9029                }
9030                "capacity" => {
9031                    node.capacity = self.parse_optional_int();
9032                }
9033                "lifetime" => {
9034                    let lt_tok = self.consume_any_ident_or_kw()?;
9035                    let lt = lt_tok.value;
9036                    if !matches!(lt.as_str(), "linear" | "affine" | "persistent") {
9037                        return Err(ParseError {
9038                            message: format!(
9039                                "Invalid lifetime '{lt}' in resource '{}' — \
9040                                 expected linear | affine | persistent",
9041                                node.name
9042                            ),
9043                            line: lt_tok.line,
9044                            column: lt_tok.column,
9045                                                    ..Default::default()
9046                        });
9047                    }
9048                    node.lifetime = lt;
9049                }
9050                "certainty_floor" => {
9051                    node.certainty_floor = self.parse_optional_float();
9052                }
9053                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
9054                // v2.67.0 — `within: <fabric>`. ONE field, so a resource
9055                // cannot be in two fabrics: Separation-Logic disjointness is
9056                // unrepresentable rather than verified.
9057                "within" => node.within = self.consume_any_ident_or_kw()?.value,
9058                // v2.67.0 — an unknown field is a HARD ERROR, not a shrug.
9059                //
9060                // This arm used to be `_ => self.skip_value()`. That is the same
9061                // family as v2.67.0's root cause (`parse_block_step` →
9062                // `skip_braced_block()`, which silently killed four primitives):
9063                // a misspelled `withn:` would have been swallowed without a
9064                // word, and the resource would have governed nothing while
9065                // looking governed. A field the parser does not know is a field
9066                // the adopter believes in and the compiler does not.
9067                unknown => {
9068                    return Err(ParseError {
9069                        message: format!(
9070                            "Unknown field '{unknown}' in resource '{}' — expected one of: \
9071                             kind, endpoint, capacity, lifetime, certainty_floor, shield, within",
9072                            node.name
9073                        ),
9074                        line: field_tok.line,
9075                        column: field_tok.column,
9076                        ..Default::default()
9077                    });
9078                }
9079            }
9080        }
9081        self.consume(TokenType::RBrace)?;
9082        Ok(node)
9083    }
9084
9085    /// Parse: `fabric Name { provider, region, zones, ephemeral, shield }`.
9086    fn parse_fabric(&mut self) -> Result<FabricDefinition, ParseError> {
9087        let tok = self.consume(TokenType::Fabric)?;
9088        let name = self.consume(TokenType::Identifier)?.value;
9089        let mut node = FabricDefinition {
9090            name,
9091            provider: String::new(),
9092            region: String::new(),
9093            zones: None,
9094            ephemeral: None,
9095            shield_ref: String::new(),
9096            loc: Loc {
9097                line: tok.line,
9098                column: tok.column,
9099            },
9100            leading_trivia: Vec::new(),
9101            trailing_trivia: Vec::new(),
9102        };
9103        self.consume(TokenType::LBrace)?;
9104        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9105            let field_name = self.current().value.clone();
9106            self.advance();
9107            if !self.check(TokenType::Colon) {
9108                if self.check(TokenType::LBrace) {
9109                    self.skip_braced_block()?;
9110                }
9111                continue;
9112            }
9113            self.advance(); // past ':'
9114            match field_name.as_str() {
9115                "provider" => node.provider = self.consume_any_ident_or_kw()?.value,
9116                "region" => node.region = self.consume(TokenType::StringLit)?.value,
9117                "zones" => node.zones = self.parse_optional_int(),
9118                "ephemeral" => {
9119                    let b = self.parse_bool()?;
9120                    node.ephemeral = Some(b);
9121                }
9122                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
9123                _ => self.skip_value(),
9124            }
9125        }
9126        self.consume(TokenType::RBrace)?;
9127        Ok(node)
9128    }
9129
9130    /// Parse: `manifest Name { resources, fabric, region, zones, compliance }`.
9131    fn parse_manifest(&mut self) -> Result<ManifestDefinition, ParseError> {
9132        let tok = self.consume(TokenType::Manifest)?;
9133        let name = self.consume(TokenType::Identifier)?.value;
9134        let mut node = ManifestDefinition {
9135            name,
9136            resources: Vec::new(),
9137            fabric_ref: String::new(),
9138            region: String::new(),
9139            zones: None,
9140            compliance: Vec::new(),
9141            loc: Loc {
9142                line: tok.line,
9143                column: tok.column,
9144            },
9145            leading_trivia: Vec::new(),
9146            trailing_trivia: Vec::new(),
9147        };
9148        self.consume(TokenType::LBrace)?;
9149        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9150            let field_name = self.current().value.clone();
9151            self.advance();
9152            if !self.check(TokenType::Colon) {
9153                if self.check(TokenType::LBrace) {
9154                    self.skip_braced_block()?;
9155                }
9156                continue;
9157            }
9158            self.advance();
9159            match field_name.as_str() {
9160                "resources" => node.resources = self.parse_bracketed_identifiers()?,
9161                "fabric" => node.fabric_ref = self.consume_any_ident_or_kw()?.value,
9162                "region" => node.region = self.consume(TokenType::StringLit)?.value,
9163                "zones" => node.zones = self.parse_optional_int(),
9164                "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
9165                _ => self.skip_value(),
9166            }
9167        }
9168        self.consume(TokenType::RBrace)?;
9169        Ok(node)
9170    }
9171
9172    /// Parse: `observe Name from Manifest { sources, quorum, timeout, on_partition, certainty_floor }`.
9173    fn parse_observe(&mut self) -> Result<ObserveDefinition, ParseError> {
9174        let tok = self.consume(TokenType::Observe)?;
9175        let name = self.consume(TokenType::Identifier)?.value;
9176        // `from <Manifest>` — required per Python grammar.
9177        self.consume(TokenType::From)?;
9178        let target = self.consume(TokenType::Identifier)?.value;
9179        let mut node = ObserveDefinition {
9180            name,
9181            target,
9182            sources: Vec::new(),
9183            quorum: None,
9184            timeout: String::new(),
9185            on_partition: "fail".to_string(),
9186            certainty_floor: None,
9187            loc: Loc {
9188                line: tok.line,
9189                column: tok.column,
9190            },
9191            leading_trivia: Vec::new(),
9192            trailing_trivia: Vec::new(),
9193        };
9194        self.consume(TokenType::LBrace)?;
9195        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9196            let field_name = self.current().value.clone();
9197            self.advance();
9198            if !self.check(TokenType::Colon) {
9199                if self.check(TokenType::LBrace) {
9200                    self.skip_braced_block()?;
9201                }
9202                continue;
9203            }
9204            self.advance();
9205            match field_name.as_str() {
9206                "sources" => node.sources = self.parse_bracketed_identifiers()?,
9207                "quorum" => node.quorum = self.parse_optional_int(),
9208                "timeout" => {
9209                    let t = self.current().clone();
9210                    match t.ttype {
9211                        TokenType::Duration | TokenType::StringLit => {
9212                            self.advance();
9213                            node.timeout = t.value;
9214                        }
9215                        _ => node.timeout = self.consume_any_ident_or_kw()?.value,
9216                    }
9217                }
9218                "on_partition" => {
9219                    let p_tok = self.consume_any_ident_or_kw()?;
9220                    let p = p_tok.value;
9221                    if !matches!(p.as_str(), "fail" | "shield_quarantine") {
9222                        return Err(ParseError {
9223                            message: format!(
9224                                "Invalid on_partition '{p}' in observe '{}' — \
9225                                 expected fail | shield_quarantine",
9226                                node.name
9227                            ),
9228                            line: p_tok.line,
9229                            column: p_tok.column,
9230                                                    ..Default::default()
9231                        });
9232                    }
9233                    node.on_partition = p;
9234                }
9235                "certainty_floor" => node.certainty_floor = self.parse_optional_float(),
9236                _ => self.skip_value(),
9237            }
9238        }
9239        self.consume(TokenType::RBrace)?;
9240        Ok(node)
9241    }
9242
9243    // ── v1.1.0 — Control cognitivo ─────────────────────────
9244
9245    /// Parse: `reconcile Name { observe, threshold, tolerance, on_drift, shield, mandate, max_retries }`.
9246    fn parse_reconcile(&mut self) -> Result<ReconcileDefinition, ParseError> {
9247        let tok = self.consume(TokenType::Reconcile)?;
9248        let name = self.consume(TokenType::Identifier)?.value;
9249        let mut node = ReconcileDefinition {
9250            name,
9251            observe_ref: String::new(),
9252            threshold: None,
9253            tolerance: None,
9254            on_drift: "provision".to_string(),
9255            shield_ref: String::new(),
9256            mandate_ref: String::new(),
9257            max_retries: 3,
9258            loc: Loc {
9259                line: tok.line,
9260                column: tok.column,
9261            },
9262            leading_trivia: Vec::new(),
9263            trailing_trivia: Vec::new(),
9264        };
9265        self.consume(TokenType::LBrace)?;
9266        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9267            let field_name = self.current().value.clone();
9268            self.advance();
9269            if !self.check(TokenType::Colon) {
9270                if self.check(TokenType::LBrace) {
9271                    self.skip_braced_block()?;
9272                }
9273                continue;
9274            }
9275            self.advance();
9276            match field_name.as_str() {
9277                "observe" => node.observe_ref = self.consume_any_ident_or_kw()?.value,
9278                "threshold" => node.threshold = self.parse_optional_float(),
9279                "tolerance" => node.tolerance = self.parse_optional_float(),
9280                "on_drift" => {
9281                    let d_tok = self.consume_any_ident_or_kw()?;
9282                    let d = d_tok.value;
9283                    if !matches!(d.as_str(), "provision" | "alert" | "refine") {
9284                        return Err(ParseError {
9285                            message: format!(
9286                                "Invalid on_drift '{d}' in reconcile '{}' — \
9287                                 expected provision | alert | refine",
9288                                node.name
9289                            ),
9290                            line: d_tok.line,
9291                            column: d_tok.column,
9292                                                    ..Default::default()
9293                        });
9294                    }
9295                    node.on_drift = d;
9296                }
9297                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
9298                "mandate" => node.mandate_ref = self.consume_any_ident_or_kw()?.value,
9299                "max_retries" => {
9300                    if let Some(v) = self.parse_optional_int() {
9301                        node.max_retries = v;
9302                    }
9303                }
9304                _ => self.skip_value(),
9305            }
9306        }
9307        self.consume(TokenType::RBrace)?;
9308        Ok(node)
9309    }
9310
9311    /// Parse: `lease Name { resource, duration, acquire, on_expire }`.
9312    fn parse_lease(&mut self) -> Result<LeaseDefinition, ParseError> {
9313        let tok = self.consume(TokenType::Lease)?;
9314        let name = self.consume(TokenType::Identifier)?.value;
9315        let mut node = LeaseDefinition {
9316            name,
9317            resource_ref: String::new(),
9318            duration: String::new(),
9319            acquire: "on_start".to_string(),
9320            on_expire: "anchor_breach".to_string(),
9321            loc: Loc {
9322                line: tok.line,
9323                column: tok.column,
9324            },
9325            leading_trivia: Vec::new(),
9326            trailing_trivia: Vec::new(),
9327        };
9328        self.consume(TokenType::LBrace)?;
9329        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9330            let field_name = self.current().value.clone();
9331            self.advance();
9332            if !self.check(TokenType::Colon) {
9333                if self.check(TokenType::LBrace) {
9334                    self.skip_braced_block()?;
9335                }
9336                continue;
9337            }
9338            self.advance();
9339            match field_name.as_str() {
9340                "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
9341                "duration" => {
9342                    let t = self.current().clone();
9343                    match t.ttype {
9344                        TokenType::Duration | TokenType::StringLit => {
9345                            self.advance();
9346                            node.duration = t.value;
9347                        }
9348                        _ => node.duration = self.consume_any_ident_or_kw()?.value,
9349                    }
9350                }
9351                "acquire" => {
9352                    let a_tok = self.consume_any_ident_or_kw()?;
9353                    let a = a_tok.value;
9354                    if !matches!(a.as_str(), "on_start" | "on_demand") {
9355                        return Err(ParseError {
9356                            message: format!(
9357                                "Invalid acquire '{a}' in lease '{}' — \
9358                                 expected on_start | on_demand",
9359                                node.name
9360                            ),
9361                            line: a_tok.line,
9362                            column: a_tok.column,
9363                                                    ..Default::default()
9364                        });
9365                    }
9366                    node.acquire = a;
9367                }
9368                "on_expire" => {
9369                    let e_tok = self.consume_any_ident_or_kw()?;
9370                    let e = e_tok.value;
9371                    if !matches!(e.as_str(), "anchor_breach" | "release" | "extend") {
9372                        return Err(ParseError {
9373                            message: format!(
9374                                "Invalid on_expire '{e}' in lease '{}' — \
9375                                 expected anchor_breach | release | extend",
9376                                node.name
9377                            ),
9378                            line: e_tok.line,
9379                            column: e_tok.column,
9380                                                    ..Default::default()
9381                        });
9382                    }
9383                    node.on_expire = e;
9384                }
9385                _ => self.skip_value(),
9386            }
9387        }
9388        self.consume(TokenType::RBrace)?;
9389        Ok(node)
9390    }
9391
9392    /// Parse: `ensemble Name { observations, quorum, aggregation, certainty_mode }`.
9393    fn parse_ensemble(&mut self) -> Result<EnsembleDefinition, ParseError> {
9394        let tok = self.consume(TokenType::Ensemble)?;
9395        let name = self.consume(TokenType::Identifier)?.value;
9396        let mut node = EnsembleDefinition {
9397            name,
9398            observations: Vec::new(),
9399            quorum: None,
9400            aggregation: "majority".to_string(),
9401            certainty_mode: "min".to_string(),
9402            loc: Loc {
9403                line: tok.line,
9404                column: tok.column,
9405            },
9406            leading_trivia: Vec::new(),
9407            trailing_trivia: Vec::new(),
9408        };
9409        self.consume(TokenType::LBrace)?;
9410        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9411            let field_name = self.current().value.clone();
9412            self.advance();
9413            if !self.check(TokenType::Colon) {
9414                if self.check(TokenType::LBrace) {
9415                    self.skip_braced_block()?;
9416                }
9417                continue;
9418            }
9419            self.advance();
9420            match field_name.as_str() {
9421                "observations" => node.observations = self.parse_bracketed_identifiers()?,
9422                "quorum" => node.quorum = self.parse_optional_int(),
9423                "aggregation" => {
9424                    let a_tok = self.consume_any_ident_or_kw()?;
9425                    let a = a_tok.value;
9426                    if !matches!(a.as_str(), "majority" | "weighted" | "byzantine") {
9427                        return Err(ParseError {
9428                            message: format!(
9429                                "Invalid aggregation '{a}' in ensemble '{}' — \
9430                                 expected majority | weighted | byzantine",
9431                                node.name
9432                            ),
9433                            line: a_tok.line,
9434                            column: a_tok.column,
9435                                                    ..Default::default()
9436                        });
9437                    }
9438                    node.aggregation = a;
9439                }
9440                "certainty_mode" => {
9441                    let c_tok = self.consume_any_ident_or_kw()?;
9442                    let c = c_tok.value;
9443                    if !matches!(c.as_str(), "min" | "weighted" | "harmonic") {
9444                        return Err(ParseError {
9445                            message: format!(
9446                                "Invalid certainty_mode '{c}' in ensemble '{}' — \
9447                                 expected min | weighted | harmonic",
9448                                node.name
9449                            ),
9450                            line: c_tok.line,
9451                            column: c_tok.column,
9452                                                    ..Default::default()
9453                        });
9454                    }
9455                    node.certainty_mode = c;
9456                }
9457                _ => self.skip_value(),
9458            }
9459        }
9460        self.consume(TokenType::RBrace)?;
9461        Ok(node)
9462    }
9463
9464    // ── v1.1.0 — Topology + π-calculus binary sessions ─────
9465
9466    /// Parse: `session Name { role1: [step, …]  role2: [step, …] }`.
9467    ///
9468    /// The enclosing `parse_session_definition` disambiguates from the session
9469    /// step token `session` (which does not exist) by always entering from the
9470    /// top-level dispatch; the identifier role name is consumed after `{`.
9471    fn parse_session_definition(&mut self) -> Result<SessionDefinition, ParseError> {
9472        let tok = self.consume(TokenType::Session)?;
9473        let name = self.consume(TokenType::Identifier)?.value;
9474        let mut node = SessionDefinition {
9475            name,
9476            roles: Vec::new(),
9477            loc: Loc {
9478                line: tok.line,
9479                column: tok.column,
9480            },
9481            leading_trivia: Vec::new(),
9482            trailing_trivia: Vec::new(),
9483        };
9484        self.consume(TokenType::LBrace)?;
9485        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9486            let role_tok = self.consume_any_ident_or_kw()?;
9487            self.consume(TokenType::Colon)?;
9488            let steps = self.parse_session_steps()?;
9489            node.roles.push(SessionRole {
9490                name: role_tok.value,
9491                steps,
9492                loc: Loc {
9493                    line: role_tok.line,
9494                    column: role_tok.column,
9495                },
9496            });
9497        }
9498        self.consume(TokenType::RBrace)?;
9499        Ok(node)
9500    }
9501
9502    /// v2.4.0 — Parse a Pauli-sum observable declaration:
9503    /// ```text
9504    /// observable EnergyHamiltonian {
9505    ///     qubits: 2
9506    ///     term: 0.5 * "ZZ"
9507    ///     term: -1.2 * "XI"
9508    /// }
9509    /// ```
9510    /// `term:` is a repeatable key (one `cₖ · Pₖ` per line). The coefficient is
9511    /// a real scalar (optional leading `+`/`-`), then `*`, then a quoted Pauli
9512    /// string. The type-checker (v2.4.0) validates the closed `{I,X,Y,Z}`
9513    /// alphabet + equal lengths; real coefficients ⇒ Hermitian by construction.
9514    fn parse_observable(&mut self) -> Result<ObservableDefinition, ParseError> {
9515        let tok = self.consume(TokenType::Observable)?;
9516        let name = self.consume(TokenType::Identifier)?.value;
9517        let mut node = ObservableDefinition {
9518            name,
9519            qubits: None,
9520            terms: Vec::new(),
9521            loc: Loc {
9522                line: tok.line,
9523                column: tok.column,
9524            },
9525            leading_trivia: Vec::new(),
9526            trailing_trivia: Vec::new(),
9527        };
9528        self.consume(TokenType::LBrace)?;
9529        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9530            let key_tok = self.consume_any_ident_or_kw()?;
9531            self.consume(TokenType::Colon)?;
9532            match key_tok.value.as_str() {
9533                "qubits" => node.qubits = Some(self.consume_number()? as i64),
9534                "term" => {
9535                    let term_loc = Loc {
9536                        line: key_tok.line,
9537                        column: key_tok.column,
9538                    };
9539                    // Optional sign, then magnitude.
9540                    let mut negative = false;
9541                    if self.check(TokenType::Minus) {
9542                        self.advance();
9543                        negative = true;
9544                    } else if self.check(TokenType::Plus) {
9545                        self.advance();
9546                    }
9547                    let mag = self.consume_number()?;
9548                    let coefficient = if negative { -mag } else { mag };
9549                    // `*` separator between coefficient and Pauli string.
9550                    self.consume(TokenType::Star)?;
9551                    let pauli = self.consume(TokenType::StringLit)?.value;
9552                    node.terms.push(PauliTerm {
9553                        coefficient,
9554                        pauli,
9555                        loc: term_loc,
9556                    });
9557                }
9558                _ => self.skip_value(),
9559            }
9560        }
9561        self.consume(TokenType::RBrace)?;
9562        Ok(node)
9563    }
9564
9565    /// v2.23.0 — Parse:
9566    /// `witness Name { claim: <ref>  against: <baseline>  metric: <metric>
9567    ///                 threshold: <ε>  data: <source> }`.
9568    /// Order-free `key: value` pairs. `claim`/`against`/`metric`/`data` are bare
9569    /// identifiers (a ref or a closed-catalog keyword); `threshold` is a number.
9570    /// Well-formedness (known metric, threshold range, required fields) is the
9571    /// type-checker's job (`axon-E0790`).
9572    fn parse_witness(&mut self) -> Result<WitnessDefinition, ParseError> {
9573        let tok = self.consume(TokenType::Witness)?;
9574        let name = self.consume(TokenType::Identifier)?.value;
9575        let mut node = WitnessDefinition {
9576            name,
9577            claim: String::new(),
9578            baseline: String::new(),
9579            metric: String::new(),
9580            threshold: 0.0,
9581            data: String::new(),
9582            loc: Loc {
9583                line: tok.line,
9584                column: tok.column,
9585            },
9586            leading_trivia: Vec::new(),
9587            trailing_trivia: Vec::new(),
9588        };
9589        self.consume(TokenType::LBrace)?;
9590        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9591            let key_tok = self.consume_any_ident_or_kw()?;
9592            self.consume(TokenType::Colon)?;
9593            match key_tok.value.as_str() {
9594                "claim" => node.claim = self.consume_any_ident_or_kw()?.value,
9595                // `against` is the baseline; `against` is not a reserved keyword,
9596                // so it lexes as an identifier key here.
9597                "against" => node.baseline = self.consume_any_ident_or_kw()?.value,
9598                "metric" => node.metric = self.consume_any_ident_or_kw()?.value,
9599                "threshold" => node.threshold = self.consume_number()?,
9600                "data" => node.data = self.consume_any_ident_or_kw()?.value,
9601                _ => self.skip_value(),
9602            }
9603        }
9604        self.consume(TokenType::RBrace)?;
9605        Ok(node)
9606    }
9607
9608    /// v2.3.0 — Parse:
9609    /// `socket Name { protocol: SessionRef, backpressure: credit(n),
9610    ///               reconnect: cognitive_state, legal_basis: ... }`.
9611    /// Fields are `key: value` pairs (order-free); only `protocol` is required.
9612    fn parse_socket(&mut self) -> Result<SocketDefinition, ParseError> {
9613        let tok = self.consume(TokenType::Socket)?;
9614        let name = self.consume(TokenType::Identifier)?.value;
9615        let mut node = SocketDefinition {
9616            name,
9617            loc: Loc { line: tok.line, column: tok.column },
9618            ..Default::default()
9619        };
9620        self.consume(TokenType::LBrace)?;
9621        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9622            let key = self.consume_any_ident_or_kw()?.value;
9623            self.consume(TokenType::Colon)?;
9624            match key.as_str() {
9625                "protocol" => node.protocol = self.consume_any_ident_or_kw()?.value,
9626                "backpressure" => {
9627                    // `credit(n)` — the typed-resource window.
9628                    let kind = self.consume_any_ident_or_kw()?.value;
9629                    if kind != "credit" {
9630                        return Err(self.error(&format!("expected `credit(n)` for backpressure, got `{kind}`")));
9631                    }
9632                    self.consume(TokenType::LParen)?;
9633                    let n = self
9634                        .consume(TokenType::Integer)?
9635                        .value
9636                        .parse::<i64>()
9637                        .map_err(|_| self.error("backpressure credit must be an integer"))?;
9638                    self.consume(TokenType::RParen)?;
9639                    node.backpressure_credit = Some(n);
9640                }
9641                "reconnect" => {
9642                    let mode = self.consume_any_ident_or_kw()?.value;
9643                    node.reconnect = mode == "cognitive_state";
9644                }
9645                "legal_basis" => node.legal_basis = Some(self.consume_any_ident_or_kw()?.value),
9646                other => return Err(self.error(&format!("unknown socket field `{other}`"))),
9647            }
9648            // Optional comma between fields.
9649            if self.check(TokenType::Comma) {
9650                self.consume(TokenType::Comma)?;
9651            }
9652        }
9653        self.consume(TokenType::RBrace)?;
9654        Ok(node)
9655    }
9656
9657    /// v2.37.0 — parse `upstream Name [from Preset@vN] { fields }`.
9658    ///
9659    /// Field grammar per `the design plan` section 1–2. The
9660    /// parser fixes the *shape* only; catalog membership (`transport:`,
9661    /// `auth:`, `overflow:`, `on_exhausted:`), key charsets and projection
9662    /// totality are v2.37.0 type-checker laws (T849–T851), mirroring how
9663    /// `socket` splits parse vs. check.
9664    fn parse_upstream(&mut self) -> Result<UpstreamDefinition, ParseError> {
9665        let tok = self.consume(TokenType::Upstream)?;
9666        let name = self.consume(TokenType::Identifier)?.value;
9667        let mut node = UpstreamDefinition {
9668            name,
9669            loc: Loc { line: tok.line, column: tok.column },
9670            ..Default::default()
9671        };
9672        // v2.37.0 — preset instantiation: `upstream X from DeepgramSTT@v1 {…}`.
9673        if self.check(TokenType::From) {
9674            self.advance();
9675            let base = self.consume(TokenType::Identifier)?.value;
9676            self.consume(TokenType::At)?;
9677            let version = self.consume_any_ident_or_kw()?.value;
9678            node.preset = Some(format!("{base}@{version}"));
9679        }
9680        self.consume(TokenType::LBrace)?;
9681        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9682            let key = self.consume_any_ident_or_kw()?.value;
9683            self.consume(TokenType::Colon)?;
9684            match key.as_str() {
9685                "transport" => node.transport = self.consume_any_ident_or_kw()?.value,
9686                "protocol" => node.protocol = self.consume_any_ident_or_kw()?.value,
9687                "role" => node.role = self.consume_any_ident_or_kw()?.value,
9688                "resolve" => node.resolve = self.parse_dotted_identifier()?,
9689                // v2.69.0 — the upstream's channel rides a declared
9690                // `resource`; the address + instance bound DERIVE from it.
9691                // XOR with `resolve:` is axon-T951 (type-checker territory).
9692                "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
9693                "secret" => node.secret = self.parse_dotted_identifier()?,
9694                "auth" => {
9695                    // `header("Name")` | `header("Name", "Prefix ")` |
9696                    // `query("param")` | `signed_url`.
9697                    node.auth_kind = self.consume_any_ident_or_kw()?.value;
9698                    if self.check(TokenType::LParen) {
9699                        self.consume(TokenType::LParen)?;
9700                        node.auth_name = Some(self.consume(TokenType::StringLit)?.value);
9701                        if self.check(TokenType::Comma) {
9702                            self.consume(TokenType::Comma)?;
9703                            node.auth_prefix = Some(self.consume(TokenType::StringLit)?.value);
9704                        }
9705                        self.consume(TokenType::RParen)?;
9706                    }
9707                }
9708                "map" => node.map = self.parse_upstream_map()?,
9709                "reconnect" => node.reconnect = Some(self.parse_upstream_reconnect()?),
9710                "overflow" => node.overflow = Some(self.consume_any_ident_or_kw()?.value),
9711                "backpressure" => {
9712                    // `credit(n)` — same typed-resource window as `socket`.
9713                    let kind = self.consume_any_ident_or_kw()?.value;
9714                    if kind != "credit" {
9715                        return Err(self.error(&format!("expected `credit(n)` for backpressure, got `{kind}`")));
9716                    }
9717                    self.consume(TokenType::LParen)?;
9718                    let n = self
9719                        .consume(TokenType::Integer)?
9720                        .value
9721                        .parse::<i64>()
9722                        .map_err(|_| self.error("backpressure credit must be an integer"))?;
9723                    self.consume(TokenType::RParen)?;
9724                    node.backpressure_credit = Some(n);
9725                }
9726                other => return Err(self.error(&format!("unknown upstream field `{other}`"))),
9727            }
9728            // Optional comma between fields.
9729            if self.check(TokenType::Comma) {
9730                self.consume(TokenType::Comma)?;
9731            }
9732        }
9733        self.consume(TokenType::RBrace)?;
9734        Ok(node)
9735    }
9736
9737    /// v2.38.0 — parse `cors Name { fields }`. Field-shape checks
9738    /// (wildcard+credentials, origin-glob shape, closed method catalog,
9739    /// cross-method path consistency) are v2.38.0 type-checker territory
9740    /// (T853-T857); the parser only builds the structural AST.
9741    ///
9742    /// **Unknown fields are a hard error** (the design decision, not `shield`'s lenient
9743    /// `axon-W010` record-and-skip) — mirrors `upstream`'s stricter
9744    /// posture, appropriate for a security-relevant declaration.
9745    fn parse_cors(&mut self) -> Result<CorsDefinition, ParseError> {
9746        let tok = self.consume(TokenType::Cors)?;
9747        let name = self.consume(TokenType::Identifier)?.value;
9748        let mut node = CorsDefinition {
9749            name,
9750            loc: Loc { line: tok.line, column: tok.column },
9751            ..Default::default()
9752        };
9753        self.consume(TokenType::LBrace)?;
9754        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9755            let key = self.consume_any_ident_or_kw()?.value;
9756            self.consume(TokenType::Colon)?;
9757            match key.as_str() {
9758                "allow_origins" => node.allow_origins = self.parse_bracketed_strings()?,
9759                "allow_methods" => node.allow_methods = self.parse_bracketed_identifiers()?,
9760                "allow_headers" => node.allow_headers = self.parse_bracketed_strings()?,
9761                "allow_credentials" => {
9762                    node.allow_credentials = self.consume_any_ident_or_kw()?.value == "true"
9763                }
9764                "max_age" => node.max_age = Some(self.consume(TokenType::Duration)?.value),
9765                "expose_headers" => node.expose_headers = self.parse_bracketed_strings()?,
9766                other => return Err(self.error(&format!("unknown cors field `{other}`"))),
9767            }
9768            // Optional comma between fields.
9769            if self.check(TokenType::Comma) {
9770                self.consume(TokenType::Comma)?;
9771            }
9772        }
9773        self.consume(TokenType::RBrace)?;
9774        Ok(node)
9775    }
9776
9777    /// v2.46.0 — parse `credential Name { ttl: grants: }`. Strict
9778    /// closed-catalog (unknown field is a hard error, the v2.38.0 the design decision
9779    /// discipline — a credential contract governs AUTHORITY, so a typo can
9780    /// never silently produce a permissive contract). `grants:` slugs are
9781    /// validated at parse time with the same closed grammar as
9782    /// `axonendpoint requires:`; the cross-field laws (non-empty grants,
9783    /// TTL bounds) are v2.46.0 type-checker territory (`axon-T893`/`T894`).
9784    fn parse_credential(&mut self) -> Result<CredentialDefinition, ParseError> {
9785        let tok = self.consume(TokenType::Credential)?;
9786        let name = self.consume(TokenType::Identifier)?.value;
9787        let mut node = CredentialDefinition {
9788            name,
9789            loc: Loc { line: tok.line, column: tok.column },
9790            ..Default::default()
9791        };
9792        self.consume(TokenType::LBrace)?;
9793        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9794            let key = self.consume_any_ident_or_kw()?.value;
9795            self.consume(TokenType::Colon)?;
9796            match key.as_str() {
9797                "ttl" => node.ttl = self.consume(TokenType::Duration)?.value,
9798                "grants" => {
9799                    let bracket_tok = self.current().clone();
9800                    let items = self.parse_bracketed_dot_identifiers()?;
9801                    for slug in &items {
9802                        if !is_valid_capability_slug(slug) {
9803                            return Err(ParseError {
9804                                message: format!(
9805                                    "Invalid capability slug '{slug}' in credential '{}' \
9806                                     `grants:`. Capability slugs must match \
9807                                     ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
9808                                     lowercase identifiers starting with a letter. Examples: \
9809                                     `chat.invoke`, `flow.execute`.",
9810                                    node.name
9811                                ),
9812                                line: bracket_tok.line,
9813                                column: bracket_tok.column,
9814                                ..Default::default()
9815                            });
9816                        }
9817                    }
9818                    node.grants = items;
9819                }
9820                other => return Err(self.error(&format!("unknown credential field `{other}`"))),
9821            }
9822            // Optional comma between fields.
9823            if self.check(TokenType::Comma) {
9824                self.consume(TokenType::Comma)?;
9825            }
9826        }
9827        self.consume(TokenType::RBrace)?;
9828        Ok(node)
9829    }
9830
9831    /// v2.40.0 — parse `cache Name { backend:, ttl:, key:, default:,
9832    /// apply_to_effects:, invalidate_on: }`. Strict closed-catalog (unknown
9833    /// field is a hard error, the v2.38.0 the design decision discipline — a cache governs
9834    /// correctness, so a typo can never silently mean "no policy"). All
9835    /// cross-field laws (single default, non-pure-needs-ttl, reference
9836    /// resolution, effect widening) are v2.40.0 type-checker territory.
9837    fn parse_cache(&mut self) -> Result<CacheDefinition, ParseError> {
9838        let tok = self.consume(TokenType::Cache)?;
9839        let name = self.consume(TokenType::Identifier)?.value;
9840        let mut node = CacheDefinition {
9841            name,
9842            loc: Loc { line: tok.line, column: tok.column },
9843            ..Default::default()
9844        };
9845        self.consume(TokenType::LBrace)?;
9846        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9847            let key = self.consume_any_ident_or_kw()?.value;
9848            self.consume(TokenType::Colon)?;
9849            match key.as_str() {
9850                "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
9851                "ttl" => node.ttl = Some(self.consume(TokenType::Duration)?.value),
9852                "key" => node.key_params = self.parse_bracketed_identifiers()?,
9853                "default" => {
9854                    node.default_policy = self.consume_any_ident_or_kw()?.value == "true"
9855                }
9856                "apply_to_effects" => {
9857                    node.apply_to_effects = self.parse_bracketed_identifiers()?
9858                }
9859                "invalidate_on" => node.invalidate_on = self.parse_bracketed_identifiers()?,
9860                other => return Err(self.error(&format!("unknown cache field `{other}`"))),
9861            }
9862            if self.check(TokenType::Comma) {
9863                self.consume(TokenType::Comma)?;
9864            }
9865        }
9866        self.consume(TokenType::RBrace)?;
9867        Ok(node)
9868    }
9869
9870    // ── v2.53.0 — Native Document Synthesis ─────────────────────────────
9871
9872    /// v2.53.0 — parse `document <Name> { target:, template:?, provenance:?,
9873    /// effects:?, <body blocks> }`. Document-level scalars are handled here;
9874    /// anything of the form `ident { … }` is a body block ([`parse_doc_block_body`]).
9875    /// Unknown scalar fields are a hard error (the v2.38.0/v2.39.0 closed-catalog
9876    /// discipline); the per-`target` block vocabulary is the v2.53.0 checker's job.
9877    fn parse_document(&mut self) -> Result<crate::ast::DocumentDefinition, ParseError> {
9878        let tok = self.consume(TokenType::Document)?;
9879        let name = self.consume(TokenType::Identifier)?.value;
9880        let mut node = crate::ast::DocumentDefinition {
9881            name,
9882            loc: Loc {
9883                line: tok.line,
9884                column: tok.column,
9885            },
9886            ..Default::default()
9887        };
9888        self.consume(TokenType::LBrace)?;
9889        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9890            let field = self.current().clone();
9891            let field_name = field.value.clone();
9892            self.advance();
9893            if self.check(TokenType::Colon) {
9894                self.advance();
9895                match field_name.as_str() {
9896                    "target" => node.target = self.consume_any_ident_or_kw()?.value,
9897                    "template" => node.template = self.parse_dotted_identifier()?,
9898                    "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
9899                    "effects" => node.effects = Some(self.parse_effect_row()?),
9900                    other => {
9901                        return Err(self.error(&format!(
9902                            "unknown document field `{other}` in document `{}` — expected \
9903                             `target:` / `template:` / `provenance:` / `effects:`, or a body \
9904                             block (`section {{ … }}` / `slide {{ … }}` / `sheet {{ … }}`)",
9905                            node.name
9906                        )))
9907                    }
9908                }
9909            } else if self.check(TokenType::LBrace) {
9910                node.blocks
9911                    .push(self.parse_doc_block_body(field_name, field.line, field.column)?);
9912            } else {
9913                return Err(self.error(&format!(
9914                    "unexpected `{field_name}` in document `{}` body — expected a `field:` or a \
9915                     body block `{field_name} {{ … }}`",
9916                    node.name
9917                )));
9918            }
9919            if self.check(TokenType::Comma) {
9920                self.advance();
9921            }
9922        }
9923        self.consume(TokenType::RBrace)?;
9924        Ok(node)
9925    }
9926
9927    /// v2.53.0 — parse a document body block whose `kind` was already
9928    /// consumed: `{ (field: value | nested-block { … })* }`. Recursive — a
9929    /// `section` holds `para`/`table`/`chart`; a `slide` holds `bullets`/
9930    /// `notes`; a `sheet` holds `row`/`formula`. A member is a field iff a
9931    /// `:` follows its name; else it must open a nested block (`{`).
9932    fn parse_doc_block_body(
9933        &mut self,
9934        kind: String,
9935        line: u32,
9936        column: u32,
9937    ) -> Result<crate::ast::DocBlock, ParseError> {
9938        let mut block = crate::ast::DocBlock {
9939            kind,
9940            loc: Loc { line, column },
9941            ..Default::default()
9942        };
9943        self.consume(TokenType::LBrace)?;
9944        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9945            let name_tok = self.current().clone();
9946            let name = self.consume_any_ident_or_kw()?.value;
9947            if self.check(TokenType::Colon) {
9948                self.advance();
9949                let value = self.parse_doc_scalar()?;
9950                block.fields.push((name, value));
9951            } else if self.check(TokenType::LBrace) {
9952                let child = self.parse_doc_block_body(name, name_tok.line, name_tok.column)?;
9953                block.children.push(child);
9954            } else {
9955                return Err(self.error(&format!(
9956                    "in document block `{}`: `{name}` must be a `field:` value or open a nested \
9957                     block `{name} {{ … }}`",
9958                    block.kind
9959                )));
9960            }
9961            if self.check(TokenType::Comma) {
9962                self.advance();
9963            }
9964        }
9965        self.consume(TokenType::RBrace)?;
9966        Ok(block)
9967    }
9968
9969    /// v2.53.0 — parse a document field value into a [`crate::ast::DocScalar`].
9970    /// A bare identifier is a REFERENCE (`text: revenue_summary`) — this is what
9971    /// the assertion-laundering barrier inspects; a quoted string / int / bool /
9972    /// bracketed list are literals.
9973    fn parse_doc_scalar(&mut self) -> Result<crate::ast::DocScalar, ParseError> {
9974        let tok = self.current().clone();
9975        match tok.ttype {
9976            TokenType::StringLit => {
9977                self.advance();
9978                Ok(crate::ast::DocScalar::Text(tok.value))
9979            }
9980            TokenType::Integer => {
9981                self.advance();
9982                Ok(crate::ast::DocScalar::Int(tok.value.parse::<i64>().unwrap_or(0)))
9983            }
9984            TokenType::Bool => {
9985                self.advance();
9986                Ok(crate::ast::DocScalar::Bool(tok.value == "true"))
9987            }
9988            TokenType::LBracket => {
9989                let items = self.parse_bracketed_strings()?;
9990                Ok(crate::ast::DocScalar::List(items))
9991            }
9992            _ => {
9993                let name = self.consume_any_ident_or_kw()?.value;
9994                Ok(crate::ast::DocScalar::Ref(name))
9995            }
9996        }
9997    }
9998
9999    // ── v2.60.0 — Governed CRM Delivery ──────────────────────────────────
10000
10001    /// v2.60.0 — parse `deliver <Name> { target:, provenance:?, secret:,
10002    /// effects:?, <operation blocks> }`. Delivery-level scalars are handled here;
10003    /// anything of the form `ident { … }` is an operation block
10004    /// ([`parse_deliver_op`]). Unknown scalar fields are a hard error (the v2.53.0
10005    /// v2.66.0 — the governed human-notification declaration:
10006    ///
10007    /// ```text
10008    /// notify LowSales {
10009    ///     channel:    sms | whatsapp | telegram
10010    ///     to:         secret(ops.oncall_phone)
10011    ///     template:   "Ventas 7d: ${resumen}"
10012    ///     window:     4h
10013    ///     provenance: attached | cleared
10014    ///     effects:    <web>
10015    /// }
10016    /// ```
10017    ///
10018    /// The closed-field discipline (v2.53.0/v2.60.0): an unknown scalar field is
10019    /// a hard parse error. The LAWS (T933/T934/T935) live in the checker
10020    /// so violations accumulate; the parser records shape (including a
10021    /// literal `to:` — kept so T934 can refuse it TEACHING the custody
10022    /// form, instead of a bare parse error).
10023    fn parse_notify(&mut self) -> Result<crate::ast::NotifyDefinition, ParseError> {
10024        let tok = self.consume(TokenType::Notify)?;
10025        let name = self.consume(TokenType::Identifier)?.value;
10026        let mut node = crate::ast::NotifyDefinition {
10027            name,
10028            loc: Loc {
10029                line: tok.line,
10030                column: tok.column,
10031            },
10032            ..Default::default()
10033        };
10034        self.consume(TokenType::LBrace)?;
10035        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10036            let field = self.current().clone();
10037            let field_name = field.value.clone();
10038            self.advance();
10039            if self.check(TokenType::Colon) {
10040                self.advance();
10041                match field_name.as_str() {
10042                    "channel" => node.channel = self.consume_any_ident_or_kw()?.value,
10043                    "to" => {
10044                        // The custody form: `secret(<dotted-class>)`. A string
10045                        // literal parses too — the checker refuses it (T934)
10046                        // with the teaching message.
10047                        if self.current().value == "secret" && self.peek_is_lparen() {
10048                            self.advance(); // `secret`
10049                            self.consume(TokenType::LParen)?;
10050                            node.to_secret = self.parse_dotted_identifier()?;
10051                            self.consume(TokenType::RParen)?;
10052                            node.to_is_secret = true;
10053                        } else if self.check(TokenType::StringLit) {
10054                            node.to_secret = self.consume(TokenType::StringLit)?.value.clone();
10055                            node.to_is_secret = false;
10056                        } else {
10057                            node.to_secret = self.consume_any_ident_or_kw()?.value.clone();
10058                            node.to_is_secret = false;
10059                        }
10060                    }
10061                    "template" => {
10062                        node.template = self.consume(TokenType::StringLit)?.value.clone()
10063                    }
10064                    "window" => {
10065                        // `4h` lexes as Integer + ident or one ident — accept
10066                        // both spellings, normalized to the joined form.
10067                        if self.check(TokenType::Integer) {
10068                            let n = self.consume(TokenType::Integer)?.value.clone();
10069                            let unit = self.consume_any_ident_or_kw()?.value.clone();
10070                            node.window = format!("{n}{unit}");
10071                        } else {
10072                            node.window = self.consume_any_ident_or_kw()?.value.clone();
10073                        }
10074                    }
10075                    "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
10076                    "effects" => node.effects = Some(self.parse_effect_row()?),
10077                    other => {
10078                        return Err(self.error(&format!(
10079                            "unknown notify field `{other}` in notify `{}` — expected \
10080                             `channel:` / `to:` / `template:` / `window:` / `provenance:` / \
10081                             `effects:`",
10082                            node.name
10083                        )))
10084                    }
10085                }
10086            }
10087        }
10088        self.consume(TokenType::RBrace)?;
10089        Ok(node)
10090    }
10091
10092    /// v2.66.0 — one-token lookahead helper for the `secret(` form.
10093    /// v2.69.0 — is the NEXT token an identifier? (`budget <Name> { … }` vs
10094    /// a bare `budget` used as an ordinary identifier.)
10095    fn peek_is_identifier(&self) -> bool {
10096        self.tokens
10097            .get(self.pos + 1)
10098            .map(|t| t.ttype == TokenType::Identifier)
10099            .unwrap_or(false)
10100    }
10101
10102    fn peek_is_lparen(&self) -> bool {
10103        self.tokens
10104            .get(self.pos + 1)
10105            .map(|t| t.ttype == TokenType::LParen)
10106            .unwrap_or(false)
10107    }
10108
10109    /// closed-catalog discipline); the operation vocabulary is the checker's job.
10110    fn parse_deliver(&mut self) -> Result<crate::ast::DeliverDefinition, ParseError> {
10111        let tok = self.consume(TokenType::Deliver)?;
10112        let name = self.consume(TokenType::Identifier)?.value;
10113        let mut node = crate::ast::DeliverDefinition {
10114            name,
10115            loc: Loc {
10116                line: tok.line,
10117                column: tok.column,
10118            },
10119            ..Default::default()
10120        };
10121        self.consume(TokenType::LBrace)?;
10122        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10123            let field = self.current().clone();
10124            let field_name = field.value.clone();
10125            self.advance();
10126            if self.check(TokenType::Colon) {
10127                self.advance();
10128                match field_name.as_str() {
10129                    "target" => node.target = self.consume_any_ident_or_kw()?.value,
10130                    "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
10131                    "secret" => node.secret = self.consume_any_ident_or_kw()?.value,
10132                    "effects" => node.effects = Some(self.parse_effect_row()?),
10133                    other => {
10134                        return Err(self.error(&format!(
10135                            "unknown deliver field `{other}` in deliver `{}` — expected \
10136                             `target:` / `provenance:` / `secret:` / `effects:`, or an operation \
10137                             block (`upsert_contact {{ … }}` / `create_deal {{ … }}` / \
10138                             `add_note {{ … }}`)",
10139                            node.name
10140                        )))
10141                    }
10142                }
10143            } else if self.check(TokenType::LBrace) {
10144                node.ops
10145                    .push(self.parse_deliver_op(field_name, field.line, field.column)?);
10146            } else {
10147                return Err(self.error(&format!(
10148                    "unexpected `{field_name}` in deliver `{}` body — expected a `field:` or an \
10149                     operation block `{field_name} {{ … }}`",
10150                    node.name
10151                )));
10152            }
10153            if self.check(TokenType::Comma) {
10154                self.advance();
10155            }
10156        }
10157        self.consume(TokenType::RBrace)?;
10158        Ok(node)
10159    }
10160
10161    /// v2.60.0 — parse a delivery operation block whose `kind` was already
10162    /// consumed: `{ (field: value)* }`. Flat (unlike a document block, an
10163    /// operation has no nested children) — each member must be a `field: value`.
10164    fn parse_deliver_op(
10165        &mut self,
10166        kind: String,
10167        line: u32,
10168        column: u32,
10169    ) -> Result<crate::ast::DeliverOp, ParseError> {
10170        let mut op = crate::ast::DeliverOp {
10171            kind,
10172            loc: Loc { line, column },
10173            ..Default::default()
10174        };
10175        self.consume(TokenType::LBrace)?;
10176        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10177            let name = self.consume_any_ident_or_kw()?.value;
10178            self.consume(TokenType::Colon).map_err(|_| {
10179                self.error(&format!(
10180                    "in deliver operation `{}`: `{name}` must be a `field: value` pair — an \
10181                     operation binds scalar fields, it takes no nested blocks",
10182                    op.kind
10183                ))
10184            })?;
10185            let value = self.parse_doc_scalar()?;
10186            op.fields.push((name, value));
10187            if self.check(TokenType::Comma) {
10188                self.advance();
10189            }
10190        }
10191        self.consume(TokenType::RBrace)?;
10192        Ok(op)
10193    }
10194
10195    /// v2.42.0 — parse `savant <Name> { domain:, cognition{…}, memory{…},
10196    /// budget{…}, mandate <M> {…} … }`. The block surface only; catalog +
10197    /// ref-resolution + budget/interruptibility binding is the v2.42.0 checker's
10198    /// job (the standing parse/check split). Unknown fields are a hard error
10199    ///: a savant governs an expensive autonomous process.
10200    fn parse_savant(&mut self) -> Result<SavantDefinition, ParseError> {
10201        let tok = self.consume(TokenType::Savant)?;
10202        let name = self.consume(TokenType::Identifier)?.value;
10203        let mut node = SavantDefinition {
10204            name,
10205            loc: Loc {
10206                line: tok.line,
10207                column: tok.column,
10208            },
10209            ..Default::default()
10210        };
10211        self.consume(TokenType::LBrace)?;
10212        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10213            let field = self.current().clone();
10214            let field_name = field.value.clone();
10215            self.advance();
10216            if self.check(TokenType::Colon) {
10217                self.advance();
10218                match field_name.as_str() {
10219                    "domain" => node.domain = self.consume(TokenType::StringLit)?.value,
10220                    other => {
10221                        return Err(self.error(&format!(
10222                            "unknown savant field `{other}` in savant `{}` — expected \
10223                             `domain:` or a `cognition` / `memory` / `budget` / `mandate` block",
10224                            node.name
10225                        )))
10226                    }
10227                }
10228            } else if field_name == "cognition" {
10229                node.cognition = Some(self.parse_savant_cognition(field.line, field.column)?);
10230            } else if field_name == "memory" {
10231                node.memory = Some(self.parse_savant_memory(field.line, field.column)?);
10232            } else if field_name == "budget" {
10233                node.budget = Some(self.parse_savant_budget(field.line, field.column)?);
10234            } else if field_name == "mandate" {
10235                node.mandates
10236                    .push(self.parse_savant_mandate(field.line, field.column)?);
10237            } else {
10238                return Err(self.error(&format!(
10239                    "unexpected `{field_name}` in savant `{}` body — expected `domain:` or a \
10240                     `cognition` / `memory` / `budget` / `mandate` block",
10241                    node.name
10242                )));
10243            }
10244            if self.check(TokenType::Comma) {
10245                self.advance();
10246            }
10247        }
10248        self.consume(TokenType::RBrace)?;
10249        Ok(node)
10250    }
10251
10252    /// v2.42.0 — the `cognition { depth:, entropic_threshold:, divergence: }`
10253    /// sub-block. Catalog validation of `depth`/`divergence` is v2.42.0.
10254    fn parse_savant_cognition(
10255        &mut self,
10256        line: u32,
10257        column: u32,
10258    ) -> Result<SavantCognition, ParseError> {
10259        self.consume(TokenType::LBrace)?;
10260        let mut node = SavantCognition {
10261            loc: Loc { line, column },
10262            ..Default::default()
10263        };
10264        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10265            let key = self.consume_any_ident_or_kw()?.value;
10266            self.consume(TokenType::Colon)?;
10267            match key.as_str() {
10268                "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
10269                "entropic_threshold" => node.entropic_threshold = self.parse_optional_float(),
10270                "divergence" => node.divergence = self.consume_any_ident_or_kw()?.value,
10271                other => {
10272                    return Err(self.error(&format!(
10273                        "unknown savant `cognition` field `{other}` — expected \
10274                         `depth` / `entropic_threshold` / `divergence`"
10275                    )))
10276                }
10277            }
10278            if self.check(TokenType::Comma) {
10279                self.advance();
10280            }
10281        }
10282        self.consume(TokenType::RBrace)?;
10283        Ok(node)
10284    }
10285
10286    /// v2.42.0 — the `memory { backend:, corpus_graph:, isolation_level: }`
10287    /// sub-block. `backend` is resolved to a declared `memory`/`corpus` in v2.42.0.
10288    fn parse_savant_memory(
10289        &mut self,
10290        line: u32,
10291        column: u32,
10292    ) -> Result<SavantMemory, ParseError> {
10293        self.consume(TokenType::LBrace)?;
10294        let mut node = SavantMemory {
10295            loc: Loc { line, column },
10296            ..Default::default()
10297        };
10298        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10299            let key = self.consume_any_ident_or_kw()?.value;
10300            self.consume(TokenType::Colon)?;
10301            match key.as_str() {
10302                "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
10303                "corpus_graph" => {
10304                    node.corpus_graph = self.consume_any_ident_or_kw()?.value == "true"
10305                }
10306                "isolation_level" => node.isolation_level = self.consume_any_ident_or_kw()?.value,
10307                other => {
10308                    return Err(self.error(&format!(
10309                        "unknown savant `memory` field `{other}` — expected \
10310                         `backend` / `corpus_graph` / `isolation_level`"
10311                    )))
10312                }
10313            }
10314            if self.check(TokenType::Comma) {
10315                self.advance();
10316            }
10317        }
10318        self.consume(TokenType::RBrace)?;
10319        Ok(node)
10320    }
10321
10322    /// v2.42.0 — the `budget { max_iterations:, max_tool_synth: }` sub-block.
10323    /// Bound to a v2.28.0 linear budget (`RateLease`) in v2.42.0.
10324    fn parse_savant_budget(
10325        &mut self,
10326        line: u32,
10327        column: u32,
10328    ) -> Result<SavantBudget, ParseError> {
10329        self.consume(TokenType::LBrace)?;
10330        let mut node = SavantBudget {
10331            loc: Loc { line, column },
10332            ..Default::default()
10333        };
10334        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10335            let key = self.consume_any_ident_or_kw()?.value;
10336            self.consume(TokenType::Colon)?;
10337            match key.as_str() {
10338                "max_iterations" => node.max_iterations = self.parse_optional_int(),
10339                "max_tool_synth" => node.max_tool_synth = self.parse_optional_int(),
10340                other => {
10341                    return Err(self.error(&format!(
10342                        "unknown savant `budget` field `{other}` — expected \
10343                         `max_iterations` / `max_tool_synth`"
10344                    )))
10345                }
10346            }
10347            if self.check(TokenType::Comma) {
10348                self.advance();
10349            }
10350        }
10351        self.consume(TokenType::RBrace)?;
10352        Ok(node)
10353    }
10354
10355    /// v2.42.0 — the `mandate <Name> { objective:, output: }` sub-block. The
10356    /// `mandate` keyword is already consumed by `parse_savant`.
10357    fn parse_savant_mandate(
10358        &mut self,
10359        line: u32,
10360        column: u32,
10361    ) -> Result<SavantMandate, ParseError> {
10362        let name = self.consume(TokenType::Identifier)?.value;
10363        let mut node = SavantMandate {
10364            name,
10365            loc: Loc { line, column },
10366            ..Default::default()
10367        };
10368        self.consume(TokenType::LBrace)?;
10369        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10370            let key = self.consume_any_ident_or_kw()?.value;
10371            self.consume(TokenType::Colon)?;
10372            match key.as_str() {
10373                "objective" => node.objective = self.consume(TokenType::StringLit)?.value,
10374                "output" => node.output_type = self.consume_any_ident_or_kw()?.value,
10375                other => {
10376                    return Err(self.error(&format!(
10377                        "unknown savant `mandate` field `{other}` — expected `objective` / `output`"
10378                    )))
10379                }
10380            }
10381            if self.check(TokenType::Comma) {
10382                self.advance();
10383            }
10384        }
10385        self.consume(TokenType::RBrace)?;
10386        Ok(node)
10387    }
10388
10389    /// v2.42.0 — parse `synth <Name> { target:, risk:, language:, sandbox:,
10390    /// review:, max_lines: }`. Flat key:value block (the `cache` shape). Catalog
10391    /// + deny-by-default validation is v2.42.0 `check_synth`. Unknown fields are a
10392    /// hard error: a synth policy governs arbitrary-code execution.
10393    fn parse_synth(&mut self) -> Result<SynthDefinition, ParseError> {
10394        let tok = self.consume(TokenType::Synth)?;
10395        let name = self.consume(TokenType::Identifier)?.value;
10396        let mut node = SynthDefinition {
10397            name,
10398            loc: Loc {
10399                line: tok.line,
10400                column: tok.column,
10401            },
10402            ..Default::default()
10403        };
10404        self.consume(TokenType::LBrace)?;
10405        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10406            let key = self.consume_any_ident_or_kw()?.value;
10407            self.consume(TokenType::Colon)?;
10408            match key.as_str() {
10409                "target" => node.target = self.consume(TokenType::StringLit)?.value,
10410                "risk" => node.risk = self.consume_any_ident_or_kw()?.value,
10411                "language" => node.language = self.consume_any_ident_or_kw()?.value,
10412                "sandbox" => node.sandbox = self.consume_any_ident_or_kw()?.value,
10413                "review" => node.review = self.consume_any_ident_or_kw()?.value,
10414                "max_lines" => node.max_lines = self.parse_optional_int(),
10415                other => {
10416                    return Err(self.error(&format!(
10417                        "unknown synth field `{other}` in synth `{}` — expected `target` / `risk` \
10418                         / `language` / `sandbox` / `review` / `max_lines`",
10419                        node.name
10420                    )))
10421                }
10422            }
10423            if self.check(TokenType::Comma) {
10424                self.consume(TokenType::Comma)?;
10425            }
10426        }
10427        self.consume(TokenType::RBrace)?;
10428        Ok(node)
10429    }
10430
10431    /// v2.37.0 — parse `voice Name { fields }`. Cross-field laws
10432    /// (stt/tts XOR realtime, interruptible ⇒ legal_basis, ref resolution)
10433    /// are v2.37.0 type-checker territory (T852), same parse/check split as
10434    /// every primitive in this file.
10435    fn parse_voice(&mut self) -> Result<VoiceDefinition, ParseError> {
10436        let tok = self.consume(TokenType::Voice)?;
10437        let name = self.consume(TokenType::Identifier)?.value;
10438        let mut node = VoiceDefinition {
10439            name,
10440            loc: Loc { line: tok.line, column: tok.column },
10441            ..Default::default()
10442        };
10443        self.consume(TokenType::LBrace)?;
10444        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10445            let key = self.consume_any_ident_or_kw()?.value;
10446            self.consume(TokenType::Colon)?;
10447            match key.as_str() {
10448                // Each leg: a declared upstream name or a `Preset@vN` ref.
10449                "stt" => node.stt = Some(self.parse_upstream_ref()?),
10450                "tts" => node.tts = Some(self.parse_upstream_ref()?),
10451                "realtime" => node.realtime = Some(self.parse_upstream_ref()?),
10452                "carrier" => node.carrier = self.consume_any_ident_or_kw()?.value,
10453                "interruptible" => {
10454                    let v = self.consume_any_ident_or_kw()?.value;
10455                    node.interruptible = v == "true";
10456                }
10457                "legal_basis" => node.legal_basis = Some(self.consume_any_ident_or_kw()?.value),
10458                "persona" => node.persona = Some(self.consume(TokenType::Identifier)?.value),
10459                "context" => node.context = Some(self.consume(TokenType::Identifier)?.value),
10460                other => return Err(self.error(&format!("unknown voice field `{other}`"))),
10461            }
10462            if self.check(TokenType::Comma) {
10463                self.consume(TokenType::Comma)?;
10464            }
10465        }
10466        self.consume(TokenType::RBrace)?;
10467        Ok(node)
10468    }
10469
10470    /// v2.37.0 — an upstream leg reference: `Ident` (a declared
10471    /// `upstream`) or `Ident@vN` (a v2.37.0 preset).
10472    fn parse_upstream_ref(&mut self) -> Result<String, ParseError> {
10473        let base = self.consume(TokenType::Identifier)?.value;
10474        if self.check(TokenType::At) {
10475            self.advance();
10476            let version = self.consume_any_ident_or_kw()?.value;
10477            Ok(format!("{base}@{version}"))
10478        } else {
10479            Ok(base)
10480        }
10481    }
10482
10483    /// v2.37.0 — parse the `map: [ rule, … ]` projection list.
10484    ///
10485    /// rule := (`send` | `receive`) <MessageType> `as` (`json` | `binary`)
10486    ///         [ `tag` <string> ]                 — send-json only
10487    ///         [ `when` <string> `=` <string> ]   — receive-json only
10488    fn parse_upstream_map(&mut self) -> Result<Vec<UpstreamMapRule>, ParseError> {
10489        self.consume(TokenType::LBracket)?;
10490        let mut rules = Vec::new();
10491        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
10492            let dir_tok = self.current().clone();
10493            let direction = match dir_tok.ttype {
10494                TokenType::Send => "send",
10495                TokenType::Receive => "receive",
10496                _ => {
10497                    return Err(self.error(&format!(
10498                        "upstream map rule must start with `send` or `receive`, got `{}`",
10499                        dir_tok.value
10500                    )))
10501                }
10502            };
10503            self.advance();
10504            let message = self.consume(TokenType::Identifier)?.value;
10505            self.consume(TokenType::As)?;
10506            let framing = self.consume_any_ident_or_kw()?.value;
10507            let mut rule = UpstreamMapRule {
10508                direction: direction.to_string(),
10509                message,
10510                framing,
10511                loc: Loc { line: dir_tok.line, column: dir_tok.column },
10512                ..Default::default()
10513            };
10514            // Optional selectors — contextual identifiers, not keywords.
10515            if self.current().value == "tag" {
10516                self.advance();
10517                rule.tag = Some(self.consume(TokenType::StringLit)?.value);
10518            } else if self.current().value == "when" {
10519                // `when "f" = "v"` — equality discriminator; `when "f"` —
10520                // field-PRESENCE discriminator (vendors like Gemini Live /
10521                // ElevenLabs mark frame kinds by which key exists, not by a
10522                // type value).
10523                self.advance();
10524                rule.when_field = Some(self.consume(TokenType::StringLit)?.value);
10525                if self.check(TokenType::Assign) {
10526                    self.advance();
10527                    rule.when_value = Some(self.consume(TokenType::StringLit)?.value);
10528                }
10529            }
10530            rules.push(rule);
10531            if self.check(TokenType::Comma) {
10532                self.advance();
10533            }
10534        }
10535        self.consume(TokenType::RBracket)?;
10536        Ok(rules)
10537    }
10538
10539    /// v2.37.0 — parse `reconnect: { backoff_ms: <int>, max_attempts:
10540    /// <int>, on_exhausted: <ident> }` (order-free, all three required —
10541    /// a reconnection policy with a hole is not a policy).
10542    fn parse_upstream_reconnect(&mut self) -> Result<UpstreamReconnect, ParseError> {
10543        self.consume(TokenType::LBrace)?;
10544        let mut backoff_ms: Option<i64> = None;
10545        let mut max_attempts: Option<i64> = None;
10546        let mut on_exhausted: Option<String> = None;
10547        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10548            let key = self.consume_any_ident_or_kw()?.value;
10549            self.consume(TokenType::Colon)?;
10550            match key.as_str() {
10551                "backoff_ms" => {
10552                    backoff_ms = Some(
10553                        self.consume(TokenType::Integer)?
10554                            .value
10555                            .parse::<i64>()
10556                            .map_err(|_| self.error("backoff_ms must be an integer"))?,
10557                    )
10558                }
10559                "max_attempts" => {
10560                    max_attempts = Some(
10561                        self.consume(TokenType::Integer)?
10562                            .value
10563                            .parse::<i64>()
10564                            .map_err(|_| self.error("max_attempts must be an integer"))?,
10565                    )
10566                }
10567                "on_exhausted" => on_exhausted = Some(self.consume_any_ident_or_kw()?.value),
10568                other => return Err(self.error(&format!("unknown reconnect field `{other}`"))),
10569            }
10570            if self.check(TokenType::Comma) {
10571                self.consume(TokenType::Comma)?;
10572            }
10573        }
10574        self.consume(TokenType::RBrace)?;
10575        match (backoff_ms, max_attempts, on_exhausted) {
10576            (Some(b), Some(m), Some(o)) => Ok(UpstreamReconnect { backoff_ms: b, max_attempts: m, on_exhausted: o }),
10577            _ => Err(self.error(
10578                "reconnect requires all of `backoff_ms:`, `max_attempts:`, `on_exhausted:` — a reconnection policy with a hole is not a policy",
10579            )),
10580        }
10581    }
10582
10583    /// Parse: `[send T, receive U, loop, end]`.
10584    fn parse_session_steps(&mut self) -> Result<Vec<SessionStep>, ParseError> {
10585        self.consume(TokenType::LBracket)?;
10586        let mut steps = Vec::new();
10587        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
10588            steps.push(self.parse_session_step()?);
10589            if self.check(TokenType::Comma) {
10590                self.advance();
10591            }
10592        }
10593        self.consume(TokenType::RBracket)?;
10594        Ok(steps)
10595    }
10596
10597    /// v2.36.0 — a **brace**-delimited session step block: `{ step, step, … }`.
10598    /// Used by the `interrupt`/`resumable` regions (the paper's block surface),
10599    /// as opposed to the `[ … ]` step-lists used by roles and choice arms.
10600    fn parse_session_step_block(&mut self) -> Result<Vec<SessionStep>, ParseError> {
10601        self.consume(TokenType::LBrace)?;
10602        let mut steps = Vec::new();
10603        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10604            steps.push(self.parse_session_step()?);
10605            if self.check(TokenType::Comma) {
10606                self.advance();
10607            }
10608        }
10609        self.consume(TokenType::RBrace)?;
10610        Ok(steps)
10611    }
10612
10613    fn parse_session_step(&mut self) -> Result<SessionStep, ParseError> {
10614        let tok = self.current().clone();
10615        let loc = Loc { line: tok.line, column: tok.column };
10616        match tok.ttype {
10617            TokenType::Send => {
10618                self.advance();
10619                let msg = self.consume_any_ident_or_kw()?;
10620                Ok(SessionStep { op: "send".into(), message_type: msg.value, loc, ..Default::default() })
10621            }
10622            TokenType::Receive => {
10623                self.advance();
10624                let msg = self.consume_any_ident_or_kw()?;
10625                Ok(SessionStep { op: "receive".into(), message_type: msg.value, loc, ..Default::default() })
10626            }
10627            TokenType::Loop => {
10628                self.advance();
10629                Ok(SessionStep { op: "loop".into(), loc, ..Default::default() })
10630            }
10631            TokenType::End => {
10632                self.advance();
10633                Ok(SessionStep { op: "end".into(), loc, ..Default::default() })
10634            }
10635            // v2.3.0 — choice: `select { ℓ: [..], … }` (⊕) | `branch { ℓ: [..], … }` (&).
10636            // `select`/`branch` are not keywords — they arrive as identifiers.
10637            TokenType::Identifier if tok.value == "select" || tok.value == "branch" => {
10638                self.parse_session_choice(&tok.value, loc)
10639            }
10640            // v2.36.0 — `interrupt { <body> } on <Signal> as <sig> resumable { <handler> }`.
10641            // Contextual keyword (identifier), like `select`/`branch`.
10642            TokenType::Identifier if tok.value == "interrupt" => {
10643                self.parse_session_interrupt(loc)
10644            }
10645            // v2.36.0 — `resume`: the handler's normal exit (hand control back to
10646            // the parked body). A bare step, no payload; only meaningful inside an
10647            // `interrupt` handler (enforced at type-check, v2.36.0).
10648            //
10649            // ⚠️ v2.87.0 — this guard used to require `TokenType::Identifier`,
10650            // and `resume` became a HARD KEYWORD when the algebraic-effect
10651            // constructs landed. The session `resume` is a DIFFERENT `resume`
10652            // (v2.36.0's interrupt-handler exit, not v2.87.0's one-shot continuation
10653            // invocation), and it broke the moment the lexer stopped handing it
10654            // over as an identifier — `axon-frontend/src/voice_desugar.rs`'s own
10655            // expansion source stopped parsing.
10656            //
10657            // Matching on the VALUE rather than the token type is what keeps a
10658            // contextual keyword contextual. This was caught by the corpus gate
10659            // (`effect_grammar::a7_…`), not by review: six new hard
10660            // keywords across a 106-file `.axon` corpus is not a risk anyone
10661            // eyeballs correctly.
10662            _ if tok.value == "resume" => {
10663                self.advance();
10664                Ok(SessionStep { op: "resume".into(), loc, ..Default::default() })
10665            }
10666            _ => Err(ParseError {
10667                message: format!(
10668                    "Invalid session step '{}' — expected send | receive | loop | end | select | branch | interrupt | resume",
10669                    tok.value
10670                ),
10671                line: tok.line,
10672                column: tok.column,
10673                ..Default::default()
10674            }),
10675        }
10676    }
10677
10678    /// v2.36.0 — consume a **contextual keyword** (`on` / `as` / `resumable`):
10679    /// a token whose *value* must equal `kw`, regardless of whether the lexer
10680    /// classified it as a keyword or a bare identifier. Keeps the `interrupt`
10681    /// surface readable without minting three reserved words.
10682    fn consume_contextual(&mut self, kw: &str) -> Result<(), ParseError> {
10683        let t = self.current().clone();
10684        if t.value != kw {
10685            return Err(ParseError {
10686                message: format!("expected `{kw}` in interrupt step, got `{}`", t.value),
10687                line: t.line,
10688                column: t.column,
10689                ..Default::default()
10690            });
10691        }
10692        self.advance();
10693        Ok(())
10694    }
10695
10696    /// v2.36.0 — Parse an interruptible region:
10697    /// `interrupt { <body-steps> } on <Signal> as <sig> resumable { <handler-steps> }`.
10698    ///
10699    /// Encoded into the string-tagged `SessionStep` (mirroring the v2.3.0 choice
10700    /// shape): `op = "interrupt"`, `message_type = <Signal>` (validated against the
10701    /// closed `CallInterruptCause` catalog at type-check, v2.36.0), two labelled
10702    /// `branches` (`body`, `handler`), `binder = <sig>`, `resumable = true`.
10703    fn parse_session_interrupt(&mut self, loc: Loc) -> Result<SessionStep, ParseError> {
10704        self.advance(); // consume `interrupt`
10705        // Body region — a brace-delimited step block (the paper's `interrupt { … }`
10706        // surface; distinct from the `[ … ]` step-lists of roles/choice arms).
10707        let body = self.parse_session_step_block()?;
10708        // `on <Signal>`
10709        self.consume_contextual("on")?;
10710        let signal = self.consume_any_ident_or_kw()?;
10711        // `as <sig>`
10712        self.consume_contextual("as")?;
10713        let binder = self.consume_any_ident_or_kw()?;
10714        // `resumable { <handler> }`
10715        self.consume_contextual("resumable")?;
10716        let handler = self.parse_session_step_block()?;
10717        Ok(SessionStep {
10718            op: "interrupt".into(),
10719            message_type: signal.value,
10720            branches: vec![
10721                SessionBranch { label: "body".into(), steps: body, loc: loc.clone() },
10722                SessionBranch { label: "handler".into(), steps: handler, loc: loc.clone() },
10723            ],
10724            binder: binder.value,
10725            resumable: true,
10726            loc,
10727        })
10728    }
10729
10730    /// v2.3.0 — Parse a choice step: `select { ask: [..], cancel: [..] }`
10731    /// (or `branch { … }`). Each `label: [steps]` arm is a nested sub-protocol.
10732    fn parse_session_choice(&mut self, op: &str, loc: Loc) -> Result<SessionStep, ParseError> {
10733        self.advance(); // consume `select` / `branch`
10734        self.consume(TokenType::LBrace)?;
10735        let mut branches = Vec::new();
10736        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10737            let label_tok = self.consume_any_ident_or_kw()?;
10738            self.consume(TokenType::Colon)?;
10739            let steps = self.parse_session_steps()?;
10740            branches.push(SessionBranch {
10741                label: label_tok.value,
10742                steps,
10743                loc: Loc { line: label_tok.line, column: label_tok.column },
10744            });
10745            if self.check(TokenType::Comma) {
10746                self.advance();
10747            }
10748        }
10749        self.consume(TokenType::RBrace)?;
10750        Ok(SessionStep { op: op.to_string(), branches, loc, ..Default::default() })
10751    }
10752
10753    /// Parse: `topology Name { nodes: [A, B, …]  edges: [A -> B : Session, …] }`.
10754    fn parse_topology(&mut self) -> Result<TopologyDefinition, ParseError> {
10755        let tok = self.consume(TokenType::Topology)?;
10756        let name = self.consume(TokenType::Identifier)?.value;
10757        let mut node = TopologyDefinition {
10758            name,
10759            nodes: Vec::new(),
10760            edges: Vec::new(),
10761            loc: Loc {
10762                line: tok.line,
10763                column: tok.column,
10764            },
10765            leading_trivia: Vec::new(),
10766            trailing_trivia: Vec::new(),
10767        };
10768        self.consume(TokenType::LBrace)?;
10769        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10770            let field_name = self.current().value.clone();
10771            self.advance();
10772            if !self.check(TokenType::Colon) {
10773                if self.check(TokenType::LBrace) {
10774                    self.skip_braced_block()?;
10775                }
10776                continue;
10777            }
10778            self.advance();
10779            match field_name.as_str() {
10780                "nodes" => node.nodes = self.parse_bracketed_identifiers()?,
10781                "edges" => node.edges = self.parse_topology_edges()?,
10782                _ => self.skip_value(),
10783            }
10784        }
10785        self.consume(TokenType::RBrace)?;
10786        Ok(node)
10787    }
10788
10789    fn parse_topology_edges(&mut self) -> Result<Vec<TopologyEdge>, ParseError> {
10790        self.consume(TokenType::LBracket)?;
10791        let mut edges = Vec::new();
10792        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
10793            edges.push(self.parse_topology_edge()?);
10794            if self.check(TokenType::Comma) {
10795                self.advance();
10796            }
10797        }
10798        self.consume(TokenType::RBracket)?;
10799        Ok(edges)
10800    }
10801
10802    fn parse_topology_edge(&mut self) -> Result<TopologyEdge, ParseError> {
10803        let src_tok = self.consume_any_ident_or_kw()?;
10804        self.consume(TokenType::Arrow)?;
10805        let tgt_tok = self.consume_any_ident_or_kw()?;
10806        self.consume(TokenType::Colon)?;
10807        let sess_tok = self.consume_any_ident_or_kw()?;
10808        Ok(TopologyEdge {
10809            source: src_tok.value,
10810            target: tgt_tok.value,
10811            session_ref: sess_tok.value,
10812            loc: Loc {
10813                line: src_tok.line,
10814                column: src_tok.column,
10815            },
10816        })
10817    }
10818
10819    // ── v1.1.0 — Cognitive immune system (paper_immune_v2.md) ────
10820
10821    /// Parse: `immune Name { watch, sensitivity, baseline, window, scope, tau, decay }`.
10822    fn parse_immune(&mut self) -> Result<ImmuneDefinition, ParseError> {
10823        let tok = self.consume(TokenType::Immune)?;
10824        let name = self.consume(TokenType::Identifier)?.value;
10825        let mut node = ImmuneDefinition {
10826            name,
10827            watch: Vec::new(),
10828            sensitivity: None,
10829            baseline: "learned".to_string(),
10830            window: 100,
10831            scope: String::new(),
10832            tau: String::new(),
10833            decay: "exponential".to_string(),
10834            loc: Loc {
10835                line: tok.line,
10836                column: tok.column,
10837            },
10838            leading_trivia: Vec::new(),
10839            trailing_trivia: Vec::new(),
10840        };
10841        self.consume(TokenType::LBrace)?;
10842        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10843            let field_name = self.current().value.clone();
10844            self.advance();
10845            if !self.check(TokenType::Colon) {
10846                if self.check(TokenType::LBrace) {
10847                    self.skip_braced_block()?;
10848                }
10849                continue;
10850            }
10851            self.advance();
10852            match field_name.as_str() {
10853                "watch" => node.watch = self.parse_bracketed_identifiers()?,
10854                "sensitivity" => node.sensitivity = self.parse_optional_float(),
10855                "baseline" => node.baseline = self.consume_any_ident_or_kw()?.value,
10856                "window" => {
10857                    if let Some(v) = self.parse_optional_int() {
10858                        node.window = v;
10859                    }
10860                }
10861                "scope" => {
10862                    let s_tok = self.consume_any_ident_or_kw()?;
10863                    let s = s_tok.value;
10864                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
10865                        return Err(ParseError {
10866                            message: format!(
10867                                "Invalid scope '{s}' in immune '{}' — \
10868                                 expected tenant | flow | global",
10869                                node.name
10870                            ),
10871                            line: s_tok.line,
10872                            column: s_tok.column,
10873                                                    ..Default::default()
10874                        });
10875                    }
10876                    node.scope = s;
10877                }
10878                "tau" => {
10879                    let t = self.current().clone();
10880                    match t.ttype {
10881                        TokenType::Duration | TokenType::StringLit => {
10882                            self.advance();
10883                            node.tau = t.value;
10884                        }
10885                        _ => node.tau = self.consume_any_ident_or_kw()?.value,
10886                    }
10887                }
10888                "decay" => {
10889                    let d_tok = self.consume_any_ident_or_kw()?;
10890                    let d = d_tok.value;
10891                    if !matches!(d.as_str(), "exponential" | "linear" | "none") {
10892                        return Err(ParseError {
10893                            message: format!(
10894                                "Invalid decay '{d}' in immune '{}' — \
10895                                 expected exponential | linear | none",
10896                                node.name
10897                            ),
10898                            line: d_tok.line,
10899                            column: d_tok.column,
10900                                                    ..Default::default()
10901                        });
10902                    }
10903                    node.decay = d;
10904                }
10905                _ => self.skip_value(),
10906            }
10907        }
10908        self.consume(TokenType::RBrace)?;
10909        Ok(node)
10910    }
10911
10912    /// Parse: `reflex Name { trigger, on_level, action, scope, sla }`.
10913    fn parse_reflex(&mut self) -> Result<ReflexDefinition, ParseError> {
10914        let tok = self.consume(TokenType::Reflex)?;
10915        let name = self.consume(TokenType::Identifier)?.value;
10916        let mut node = ReflexDefinition {
10917            name,
10918            trigger: String::new(),
10919            on_level: "doubt".to_string(),
10920            action: String::new(),
10921            scope: String::new(),
10922            sla: String::new(),
10923            loc: Loc {
10924                line: tok.line,
10925                column: tok.column,
10926            },
10927            leading_trivia: Vec::new(),
10928            trailing_trivia: Vec::new(),
10929        };
10930        self.consume(TokenType::LBrace)?;
10931        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10932            let field_name = self.current().value.clone();
10933            self.advance();
10934            if !self.check(TokenType::Colon) {
10935                if self.check(TokenType::LBrace) {
10936                    self.skip_braced_block()?;
10937                }
10938                continue;
10939            }
10940            self.advance();
10941            match field_name.as_str() {
10942                "trigger" => node.trigger = self.consume_any_ident_or_kw()?.value,
10943                "on_level" => {
10944                    let l_tok = self.consume_any_ident_or_kw()?;
10945                    let l = l_tok.value;
10946                    if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
10947                        return Err(ParseError {
10948                            message: format!(
10949                                "Invalid on_level '{l}' in reflex '{}' — \
10950                                 expected know | believe | speculate | doubt",
10951                                node.name
10952                            ),
10953                            line: l_tok.line,
10954                            column: l_tok.column,
10955                                                    ..Default::default()
10956                        });
10957                    }
10958                    node.on_level = l;
10959                }
10960                "action" => {
10961                    let a_tok = self.consume_any_ident_or_kw()?;
10962                    let a = a_tok.value;
10963                    if !matches!(
10964                        a.as_str(),
10965                        "drop"
10966                            | "revoke"
10967                            | "emit"
10968                            | "redact"
10969                            | "quarantine"
10970                            | "terminate"
10971                            | "alert"
10972                    ) {
10973                        return Err(ParseError {
10974                            message: format!(
10975                                "Invalid action '{a}' in reflex '{}' — \
10976                                 expected drop | revoke | emit | redact | \
10977                                 quarantine | terminate | alert",
10978                                node.name
10979                            ),
10980                            line: a_tok.line,
10981                            column: a_tok.column,
10982                                                    ..Default::default()
10983                        });
10984                    }
10985                    node.action = a;
10986                }
10987                "scope" => {
10988                    let s_tok = self.consume_any_ident_or_kw()?;
10989                    let s = s_tok.value;
10990                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
10991                        return Err(ParseError {
10992                            message: format!(
10993                                "Invalid scope '{s}' in reflex '{}' — \
10994                                 expected tenant | flow | global",
10995                                node.name
10996                            ),
10997                            line: s_tok.line,
10998                            column: s_tok.column,
10999                                                    ..Default::default()
11000                        });
11001                    }
11002                    node.scope = s;
11003                }
11004                "sla" => {
11005                    let t = self.current().clone();
11006                    match t.ttype {
11007                        TokenType::Duration | TokenType::StringLit => {
11008                            self.advance();
11009                            node.sla = t.value;
11010                        }
11011                        _ => node.sla = self.consume_any_ident_or_kw()?.value,
11012                    }
11013                }
11014                _ => self.skip_value(),
11015            }
11016        }
11017        self.consume(TokenType::RBrace)?;
11018        Ok(node)
11019    }
11020
11021    /// Parse: `heal Name { source, on_level, mode, scope, review_sla, shield, max_patches }`.
11022    fn parse_heal(&mut self) -> Result<HealDefinition, ParseError> {
11023        let tok = self.consume(TokenType::Heal)?;
11024        let name = self.consume(TokenType::Identifier)?.value;
11025        let mut node = HealDefinition {
11026            name,
11027            source: String::new(),
11028            on_level: "doubt".to_string(),
11029            mode: "human_in_loop".to_string(),
11030            scope: String::new(),
11031            review_sla: String::new(),
11032            shield_ref: String::new(),
11033            max_patches: 3,
11034            loc: Loc {
11035                line: tok.line,
11036                column: tok.column,
11037            },
11038            leading_trivia: Vec::new(),
11039            trailing_trivia: Vec::new(),
11040        };
11041        self.consume(TokenType::LBrace)?;
11042        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11043            let field_name = self.current().value.clone();
11044            self.advance();
11045            if !self.check(TokenType::Colon) {
11046                if self.check(TokenType::LBrace) {
11047                    self.skip_braced_block()?;
11048                }
11049                continue;
11050            }
11051            self.advance();
11052            match field_name.as_str() {
11053                "source" => node.source = self.consume_any_ident_or_kw()?.value,
11054                "on_level" => {
11055                    let l_tok = self.consume_any_ident_or_kw()?;
11056                    let l = l_tok.value;
11057                    if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
11058                        return Err(ParseError {
11059                            message: format!(
11060                                "Invalid on_level '{l}' in heal '{}' — \
11061                                 expected know | believe | speculate | doubt",
11062                                node.name
11063                            ),
11064                            line: l_tok.line,
11065                            column: l_tok.column,
11066                                                    ..Default::default()
11067                        });
11068                    }
11069                    node.on_level = l;
11070                }
11071                "mode" => {
11072                    let m_tok = self.consume_any_ident_or_kw()?;
11073                    let m = m_tok.value;
11074                    if !matches!(m.as_str(), "audit_only" | "human_in_loop" | "adversarial") {
11075                        return Err(ParseError {
11076                            message: format!(
11077                                "Invalid mode '{m}' in heal '{}' — \
11078                                 expected audit_only | human_in_loop | adversarial",
11079                                node.name
11080                            ),
11081                            line: m_tok.line,
11082                            column: m_tok.column,
11083                                                    ..Default::default()
11084                        });
11085                    }
11086                    node.mode = m;
11087                }
11088                "scope" => {
11089                    let s_tok = self.consume_any_ident_or_kw()?;
11090                    let s = s_tok.value;
11091                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
11092                        return Err(ParseError {
11093                            message: format!(
11094                                "Invalid scope '{s}' in heal '{}' — \
11095                                 expected tenant | flow | global",
11096                                node.name
11097                            ),
11098                            line: s_tok.line,
11099                            column: s_tok.column,
11100                                                    ..Default::default()
11101                        });
11102                    }
11103                    node.scope = s;
11104                }
11105                "review_sla" => {
11106                    let t = self.current().clone();
11107                    match t.ttype {
11108                        TokenType::Duration | TokenType::StringLit => {
11109                            self.advance();
11110                            node.review_sla = t.value;
11111                        }
11112                        _ => node.review_sla = self.consume_any_ident_or_kw()?.value,
11113                    }
11114                }
11115                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
11116                "max_patches" => {
11117                    if let Some(v) = self.parse_optional_int() {
11118                        node.max_patches = v;
11119                    }
11120                }
11121                _ => self.skip_value(),
11122            }
11123        }
11124        self.consume(TokenType::RBrace)?;
11125        Ok(node)
11126    }
11127
11128    // ── v1.3.1 — UI cognitiva (component / view) ────────────
11129
11130    /// Parse: `component Name { renders, via_shield, on_interact, render_hint }`.
11131    fn parse_component(&mut self) -> Result<ComponentDefinition, ParseError> {
11132        let tok = self.consume(TokenType::Component)?;
11133        let name = self.consume(TokenType::Identifier)?.value;
11134        let mut node = ComponentDefinition {
11135            name,
11136            renders: String::new(),
11137            via_shield: String::new(),
11138            on_interact: String::new(),
11139            render_hint: "custom".to_string(),
11140            loc: Loc {
11141                line: tok.line,
11142                column: tok.column,
11143            },
11144            leading_trivia: Vec::new(),
11145            trailing_trivia: Vec::new(),
11146        };
11147        self.consume(TokenType::LBrace)?;
11148        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11149            let field_name = self.current().value.clone();
11150            self.advance();
11151            if !self.check(TokenType::Colon) {
11152                if self.check(TokenType::LBrace) {
11153                    self.skip_braced_block()?;
11154                }
11155                continue;
11156            }
11157            self.advance();
11158            match field_name.as_str() {
11159                "renders" => node.renders = self.consume_any_ident_or_kw()?.value,
11160                "via_shield" => node.via_shield = self.consume_any_ident_or_kw()?.value,
11161                "on_interact" => node.on_interact = self.consume_any_ident_or_kw()?.value,
11162                "render_hint" => {
11163                    let h_tok = self.consume_any_ident_or_kw()?;
11164                    let h = h_tok.value;
11165                    if !matches!(h.as_str(), "card" | "list" | "form" | "chart" | "custom") {
11166                        return Err(ParseError {
11167                            message: format!(
11168                                "Invalid render_hint '{h}' in component '{}' — \
11169                                 expected card | list | form | chart | custom",
11170                                node.name
11171                            ),
11172                            line: h_tok.line,
11173                            column: h_tok.column,
11174                                                    ..Default::default()
11175                        });
11176                    }
11177                    node.render_hint = h;
11178                }
11179                _ => self.skip_value(),
11180            }
11181        }
11182        self.consume(TokenType::RBrace)?;
11183        Ok(node)
11184    }
11185
11186    /// Parse: `view Name { title, components: [...], route }`.
11187    fn parse_view(&mut self) -> Result<ViewDefinition, ParseError> {
11188        let tok = self.consume(TokenType::View)?;
11189        let name = self.consume(TokenType::Identifier)?.value;
11190        let mut node = ViewDefinition {
11191            name,
11192            title: String::new(),
11193            components: Vec::new(),
11194            route: String::new(),
11195            loc: Loc {
11196                line: tok.line,
11197                column: tok.column,
11198            },
11199            leading_trivia: Vec::new(),
11200            trailing_trivia: Vec::new(),
11201        };
11202        self.consume(TokenType::LBrace)?;
11203        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11204            let field_name = self.current().value.clone();
11205            self.advance();
11206            if !self.check(TokenType::Colon) {
11207                if self.check(TokenType::LBrace) {
11208                    self.skip_braced_block()?;
11209                }
11210                continue;
11211            }
11212            self.advance();
11213            match field_name.as_str() {
11214                "title" => node.title = self.consume(TokenType::StringLit)?.value,
11215                "components" => node.components = self.parse_bracketed_identifiers()?,
11216                "route" => node.route = self.consume(TokenType::StringLit)?.value,
11217                _ => self.skip_value(),
11218            }
11219        }
11220        self.consume(TokenType::RBrace)?;
11221        Ok(node)
11222    }
11223
11224    fn parse_axonendpoint(&mut self) -> Result<AxonEndpointDefinition, ParseError> {
11225        let tok = self.consume(TokenType::AxonEndpoint)?;
11226        let name = self.consume(TokenType::Identifier)?.value;
11227        let mut node = AxonEndpointDefinition {
11228            name,
11229            method: String::new(),
11230            path: String::new(),
11231            body_type: String::new(),
11232            execute_flow: String::new(),
11233            output_type: String::new(),
11234            shield_ref: String::new(),
11235            // v2.38.0 — `cors:` reference; empty ≡ no cors declared
11236            // (the design decision: no CORS headers, ever — secure by default).
11237            cors_ref: String::new(),
11238            retries: None,
11239            timeout: String::new(),
11240            compliance: Vec::new(),
11241            // v1.21.0 — Defaults preserve backwards compat per D1.
11242            transport: "json".to_string(),
11243            keepalive: String::new(),
11244            // v1.22.0 — Inference fields (parser-default state).
11245            // Both fields toggle/populate only when the source provides
11246            // an explicit `transport:` declaration (parser sets
11247            // `transport_explicit = true`) AND the type-checker walks
11248            // the program to compute `implicit_transport`.
11249            transport_explicit: false,
11250            implicit_transport: String::new(),
11251            // v1.23.0 (D8) — auth scope; empty list ≡ no auth gate.
11252            requires_capabilities: Vec::new(),
11253            // v2.44.0 — explicit authorization-coverage opt-out. Default
11254            // false; the v2.44.0 rule requires coverage OR `public: true`.
11255            public: false,
11256            // v1.23.0 — Replay-token binding (D9 plan-vivo).
11257            // Parser defaults: not explicit; effective value resolved
11258            // at deploy time using the method-default heuristic.
11259            replay_explicit: false,
11260            replay: false,
11261            // v1.28.0 — Wire-format dialect default
11262            // empty; the runtime classifier resolves the default
11263            // dialect per the algebraic-effect predicate when the
11264            // source omits `transport: sse(<dialect>)`.
11265            transport_dialect: String::new(),
11266            // v1.27.1 — Algebraic-effect override.
11267            // Parser default false; populated by the type-checker's
11268            // compute_implicit_transports pass once the full program
11269            // is known (the predicate cross-references tool effects
11270            // declared anywhere in the program).
11271            has_algebraic_stream_effect: false,
11272            // v1.31.0 (D2) — declared execution backend; empty ≡
11273            // not declared (the endpoint resolves down the v1.31.0 D1
11274            // ladder). A non-empty value is validated against the
11275            // closed `AXONENDPOINT_BACKEND_VALUES` catalog below.
11276            backend: String::new(),
11277            // v1.32.0 (D1) — Path-param names extracted from the
11278            // `path:` string AFTER the field is parsed. Initialized
11279            // empty; populated by `extract_path_param_names` after
11280            // the `path:` field is read in the loop below.
11281            path_params: Vec::new(),
11282            // v1.32.0 (D2) — Inline `query: { name: Type, name: Type? }`
11283            // block. Initialized empty; populated by the `"query"` arm
11284            // in the field loop below. Closed catalog enforced at parse
11285            // time per `axonendpoint_is_valid_query_param_type`.
11286            query_params: Vec::new(),
11287            loc: Loc {
11288                line: tok.line,
11289                column: tok.column,
11290            },
11291            leading_trivia: Vec::new(),
11292            trailing_trivia: Vec::new(),
11293        };
11294        self.consume(TokenType::LBrace)?;
11295        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11296            let field_name = self.current().value.clone();
11297            self.advance();
11298            if self.check(TokenType::Colon) {
11299                self.advance();
11300                match field_name.as_str() {
11301                    "method" => {
11302                        // v1.23.0 D3 — closed method enum
11303                        // `{GET, POST, PUT, DELETE, PATCH}`. Unknown
11304                        // values rejected at parse time with smart-
11305                        // suggest hint (v1.20.0). HEAD/OPTIONS/etc.
11306                        // are runtime-managed and not adopter-
11307                        // declarable.
11308                        let value_tok = self.consume_any_ident_or_kw()?;
11309                        let value_upper = value_tok.value.to_uppercase();
11310                        if !axonendpoint_is_valid_method(&value_upper) {
11311                            let hint = crate::smart_suggest::suggest_for(
11312                                &value_upper,
11313                                AXONENDPOINT_METHOD_VALUES,
11314                            );
11315                            let base = format!(
11316                                "Invalid method '{}' in axonendpoint '{}'.",
11317                                value_tok.value, node.name
11318                            );
11319                            let message = if hint.is_empty() {
11320                                format!(
11321                                    "{base} expected GET | POST | PUT | DELETE | PATCH, found {}",
11322                                    value_tok.value
11323                                )
11324                            } else {
11325                                format!(
11326                                    "{base} {hint} (expected GET | POST | PUT | DELETE | PATCH, found {})",
11327                                    value_tok.value
11328                                )
11329                            };
11330                            return Err(ParseError {
11331                                message,
11332                                line: value_tok.line,
11333                                column: value_tok.column,
11334                                ..Default::default()
11335                            });
11336                        }
11337                        node.method = value_upper;
11338                    }
11339                    "path" => {
11340                        node.path = self.consume(TokenType::StringLit)?.value.clone();
11341                        // v1.32.0 (D1) — extract `{name}` placeholders
11342                        // for the Request Binding Contract's path-param
11343                        // source. Duplicate `{name}` in the same path
11344                        // is rejected at parse time (HTTP route patterns
11345                        // structurally reject duplicates; surfacing the
11346                        // error here is friendlier than letting axum
11347                        // panic at registration).
11348                        match extract_path_param_names(&node.path) {
11349                            Ok(names) => node.path_params = names,
11350                            Err(dup) => {
11351                                let cur = self.current().clone();
11352                                return Err(ParseError {
11353                                    message: format!(
11354                                        "axonendpoint '{}' declares path '{}' \
11355                                         containing duplicate placeholder '{{{}}}'. \
11356                                         Each `{{name}}` in a `path:` must be \
11357                                         unique — the runtime cannot bind two \
11358                                         path segments to the same name.",
11359                                        node.name, node.path, dup,
11360                                    ),
11361                                    line: cur.line,
11362                                    column: cur.column,
11363                                    ..Default::default()
11364                                });
11365                            }
11366                        }
11367                    },
11368                    "body" => node.body_type = self.consume_any_ident_or_kw()?.value.clone(),
11369                    "query" => {
11370                        // v1.32.0 (D2) — Inline query-parameter block.
11371                        // Grammar: `query: { name: Type [, name: Type?]* }`.
11372                        // Closed type catalog
11373                        // `AXONENDPOINT_QUERY_PARAM_TYPES = {Text, Int,
11374                        // Float, Bool, Uuid}`. Optional via `?` suffix
11375                        // reuses `TypeExpr.optional` semantics already in
11376                        // use for flow parameters + body type fields. A
11377                        // duplicate field name in the same block is a
11378                        // parse error (HTTP query strings DO allow
11379                        // multi-value but v1.38.5 binds the first value
11380                        // only — see plan vivo section 7 forward-compat).
11381                        //
11382                        // v1.32.0 (D2 robustness) — declaring `query:`
11383                        // twice on the same axonendpoint silently merged
11384                        // params pre-hardening. Now it's a parse error
11385                        // so an adopter typo / copy-paste mistake
11386                        // surfaces with line + column instead of
11387                        // producing an unexpectedly-augmented endpoint.
11388                        let lbrace_tok = self.consume(TokenType::LBrace)?;
11389                        let block_line = lbrace_tok.line;
11390                        if !node.query_params.is_empty() {
11391                            return Err(ParseError {
11392                                message: format!(
11393                                    "axonendpoint '{}' declares `query: {{ … }}` \
11394                                     more than once. The query-parameter block \
11395                                     is unique per endpoint; combine all params \
11396                                     into a single block.",
11397                                    node.name,
11398                                ),
11399                                line: lbrace_tok.line,
11400                                column: lbrace_tok.column,
11401                                ..Default::default()
11402                            });
11403                        }
11404                        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11405                            let name_tok = self.consume(TokenType::Identifier)?;
11406                            let field_name = name_tok.value.clone();
11407                            // Duplicate detection within the block.
11408                            if node
11409                                .query_params
11410                                .iter()
11411                                .any(|f| f.name == field_name)
11412                            {
11413                                return Err(ParseError {
11414                                    message: format!(
11415                                        "axonendpoint '{}' declares duplicate \
11416                                         query param '{}' inside `query: {{ … }}`. \
11417                                         Each name must appear at most once \
11418                                         .",
11419                                        node.name, field_name,
11420                                    ),
11421                                    line: name_tok.line,
11422                                    column: name_tok.column,
11423                                    ..Default::default()
11424                                });
11425                            }
11426                            self.consume(TokenType::Colon)?;
11427                            let type_expr = self.parse_type_expr()?;
11428                            // v1.32.0 (D2 robustness) — reject generic
11429                            // type expressions on query params. The
11430                            // closed catalog is 5 primitives; container
11431                            // types (`Optional<T>`, `List<T>`, etc.)
11432                            // would mislead the adopter into thinking
11433                            // they bind multi-value query strings
11434                            // (deferred per plan vivo section 7) or that
11435                            // `Optional<Text>` is the canonical way to
11436                            // declare an optional query (it's NOT —
11437                            // `Text?` is). Surface the canonical syntax
11438                            // verbatim so the fix is obvious.
11439                            if !type_expr.generic_param.is_empty() {
11440                                let canonical_hint = if type_expr.name == "Optional" {
11441                                    format!(
11442                                        " Use `{}?` (the `?` suffix) for an \
11443                                         optional query param instead of \
11444                                         `Optional<{}>`.",
11445                                        type_expr.generic_param,
11446                                        type_expr.generic_param,
11447                                    )
11448                                } else if type_expr.name == "List" {
11449                                    " Multi-value query params (e.g. `?tag=a&tag=b`) \
11450                                     are honest-deferred from v1.38.5; bind a \
11451                                     single-value `Text` query param and parse \
11452                                     the value inside the flow."
11453                                        .to_string()
11454                                } else {
11455                                    String::new()
11456                                };
11457                                return Err(ParseError {
11458                                    message: format!(
11459                                        "axonendpoint '{}' query param '{}' uses \
11460                                         a generic type `{}<{}>`. Query params \
11461                                         take a primitive type from the closed \
11462                                         catalog ({}); the `?` suffix marks \
11463                                         optional.{} .",
11464                                        node.name,
11465                                        field_name,
11466                                        type_expr.name,
11467                                        type_expr.generic_param,
11468                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | "),
11469                                        canonical_hint,
11470                                    ),
11471                                    line: type_expr.loc.line,
11472                                    column: type_expr.loc.column,
11473                                    ..Default::default()
11474                                });
11475                            }
11476                            // Validate against the closed catalog. A
11477                            // miss surfaces a v1.20.0-style smart-suggest
11478                            // hint when within edit-distance 2.
11479                            if !axonendpoint_is_valid_query_param_type(&type_expr.name) {
11480                                // `smart_suggest::suggest_for` returns
11481                                // pre-formatted prose like
11482                                // "Did you mean `Text`?" or
11483                                // "Did you mean `Text` or `Int`?" (empty
11484                                // when no candidate within edit-distance
11485                                // 2). Concatenate without re-wrapping.
11486                                let hint = crate::smart_suggest::suggest_for(
11487                                    &type_expr.name,
11488                                    AXONENDPOINT_QUERY_PARAM_TYPES,
11489                                );
11490                                let hint_text = if hint.is_empty() {
11491                                    format!(
11492                                        " Expected one of: {}.",
11493                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | ")
11494                                    )
11495                                } else {
11496                                    format!(
11497                                        " {} Expected one of: {}.",
11498                                        hint,
11499                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | ")
11500                                    )
11501                                };
11502                                return Err(ParseError {
11503                                    message: format!(
11504                                        "axonendpoint '{}' query param '{}' has \
11505                                         unsupported type '{}'.{} .",
11506                                        node.name, field_name, type_expr.name,
11507                                        hint_text,
11508                                    ),
11509                                    line: type_expr.loc.line,
11510                                    column: type_expr.loc.column,
11511                                    ..Default::default()
11512                                });
11513                            }
11514                            node.query_params.push(TypeField {
11515                                name: field_name,
11516                                type_expr,
11517                                loc: Loc {
11518                                    line: name_tok.line,
11519                                    column: name_tok.column,
11520                                },
11521                            });
11522                            // Trailing comma is optional; the next loop
11523                            // iteration handles `}` cleanly. Accept both
11524                            // `name: Type, name: Type` AND `name: Type
11525                            // name: Type` (the existing parser style is
11526                            // forgiving about list separators).
11527                            if self.check(TokenType::Comma) {
11528                                self.advance();
11529                            }
11530                            let _ = block_line; // suppress unused warning
11531                        }
11532                        self.consume(TokenType::RBrace)?;
11533                    },
11534                    "execute" => node.execute_flow = self.consume_any_ident_or_kw()?.value.clone(),
11535                    "output" => {
11536                        // v1.31.0 — promote axonendpoint `output:`
11537                        // parsing from a single token to the full
11538                        // generic-aware type expression (mirroring
11539                        // `parse_step` for FlowStep::Step which already
11540                        // uses `parse_output_type_string`).
11541                        //
11542                        // Pre-38.x.f: `output: List<Item>` captured only
11543                        // `"List"`, dropping `<Item>` (next tokens were
11544                        // either left unconsumed or absorbed by the
11545                        // following field). v1.39.0's narrow cardinality
11546                        // gate happened to fire correctly for `output: T`
11547                        // + retrieve-tail because the singular-detection
11548                        // path used `!starts_with("List<")` — but the
11549                        // SYMMETRIC `output: List<T>` + singular-tail
11550                        // case (38.x.f D3) needs the FULL `List<T>`
11551                        // shape captured; without it the gate sees
11552                        // `"List"` and misclassifies as Singular.
11553                        node.output_type = self.parse_output_type_string()?;
11554                    }
11555                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
11556                    // v2.38.0 — the `cors: <Name>` reference.
11557                    "cors" => node.cors_ref = self.consume_any_ident_or_kw()?.value.clone(),
11558                    "retries" => node.retries = self.parse_optional_int(),
11559                    "timeout" => {
11560                        let t = self.current().clone();
11561                        self.advance();
11562                        node.timeout = t.value.clone();
11563                    }
11564                    "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
11565                    "replay" => {
11566                        // v1.23.0 (D9 plan-vivo) — Replay-token binding.
11567                        // Boolean `replay: true | false`. Default (when
11568                        // omitted) is method-derived at deploy-time:
11569                        // POST/PUT → true, GET/DELETE → false. Explicit
11570                        // declaration sets `replay_explicit = true` so
11571                        // the runtime knows NOT to override.
11572                        let value_tok = self.consume(TokenType::Bool)?;
11573                        node.replay = value_tok.value.eq_ignore_ascii_case("true");
11574                        node.replay_explicit = true;
11575                    }
11576                    // v2.44.0 — `public: true | false`, the explicit
11577                    // authorization-coverage opt-out (doctrine
11578                    // `every_boundary_is_guarded`). Mirrors `replay:`'s bool
11579                    // parse. Default false; the v2.44.0 rule (`axon-T890`)
11580                    // requires a covering discipline OR `public: true`.
11581                    "public" => {
11582                        let value_tok = self.consume(TokenType::Bool)?;
11583                        node.public = value_tok.value.eq_ignore_ascii_case("true");
11584                    }
11585                    "requires" => {
11586                        // v1.23.0 (D8) — Auth scope per axonendpoint.
11587                        // Closed slug grammar
11588                        // `^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$` enforced
11589                        // at parse time with smart-suggest-style hint.
11590                        // Empty list means "no auth gate" (D9 backwards-
11591                        // compat). Cross-stack with Python parser.
11592                        let bracket_tok = self.current().clone();
11593                        let items = self.parse_bracketed_dot_identifiers()?;
11594                        for slug in &items {
11595                            if !is_valid_capability_slug(slug) {
11596                                return Err(ParseError {
11597                                    message: format!(
11598                                        "Invalid capability slug '{slug}' in axonendpoint '{}' \
11599                                         `requires:`. Capability slugs must match \
11600                                         ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
11601                                         lowercase identifiers starting with a letter. Examples: \
11602                                         `admin`, `legal.read`, `hipaa.phi.read`.",
11603                                        node.name
11604                                    ),
11605                                    line: bracket_tok.line,
11606                                    column: bracket_tok.column,
11607                                    ..Default::default()
11608                                });
11609                            }
11610                        }
11611                        node.requires_capabilities = items;
11612                    }
11613                    // v1.21.0 — HTTP transport enum (D2 closed) + keepalive (D6 closed).
11614                    // Mirrors `axon/compiler/parser.py` `_parse_axonendpoint`.
11615                    // Drift-gate corpus verifies byte-identical parse cross-stack.
11616                    "transport" => {
11617                        let value_tok = self.consume_any_ident_or_kw()?;
11618                        let value = &value_tok.value;
11619                        if !axonendpoint_is_valid_transport(value) {
11620                            let hint = crate::smart_suggest::suggest_for(
11621                                value,
11622                                AXONENDPOINT_TRANSPORT_VALUES,
11623                            );
11624                            let base = format!(
11625                                "Invalid transport '{}' in axonendpoint '{}'.",
11626                                value, node.name
11627                            );
11628                            let message = if hint.is_empty() {
11629                                format!("{base} expected json | sse | ndjson, found {value}")
11630                            } else {
11631                                format!(
11632                                    "{base} {hint} (expected json | sse | ndjson, found {value})"
11633                                )
11634                            };
11635                            return Err(ParseError {
11636                                message,
11637                                line: value_tok.line,
11638                                column: value_tok.column,
11639                                ..Default::default()
11640                            });
11641                        }
11642                        node.transport = value.clone();
11643                        // v1.22.0 D1 — mark the field as explicitly
11644                        // declared so the type-checker's implicit-transport
11645                        // inference knows NOT to override this value with
11646                        // the produces_stream-driven inference.
11647                        node.transport_explicit = true;
11648                        // v1.28.0 — Optional dialect
11649                        // parametrization: `transport: sse(<dialect>)`.
11650                        // Only valid when the base value is `sse`
11651                        // (json + ndjson dialects are the dialects
11652                        // themselves; `json(<x>)` / `ndjson(<x>)`
11653                        // would be parse errors caught below).
11654                        if self.check(TokenType::LParen) {
11655                            if value != "sse" {
11656                                let tok = self.current().clone();
11657                                return Err(ParseError {
11658                                    message: format!(
11659                                        "Dialect parametrization \
11660                                         `transport: {value}(<dialect>)` is \
11661                                         only valid for `sse`; got \
11662                                         `{value}` in axonendpoint '{}'.",
11663                                        node.name
11664                                    ),
11665                                    line: tok.line,
11666                                    column: tok.column,
11667                                    ..Default::default()
11668                                });
11669                            }
11670                            self.advance(); // consume LParen
11671                            let dialect_tok = self.consume_any_ident_or_kw()?;
11672                            let dialect = dialect_tok.value.clone();
11673                            if !AXONENDPOINT_TRANSPORT_DIALECTS
11674                                .iter()
11675                                .any(|&d| d == dialect)
11676                            {
11677                                let hint = crate::smart_suggest::suggest_for(
11678                                    &dialect,
11679                                    AXONENDPOINT_TRANSPORT_DIALECTS,
11680                                );
11681                                let base = format!(
11682                                    "Invalid SSE dialect '{dialect}' in axonendpoint '{}'.",
11683                                    node.name
11684                                );
11685                                let message = if hint.is_empty() {
11686                                    format!(
11687                                        "{base} expected axon | openai | kimi | glm | anthropic, found {dialect}"
11688                                    )
11689                                } else {
11690                                    format!(
11691                                        "{base} {hint} (expected axon | openai | kimi | glm | anthropic, found {dialect})"
11692                                    )
11693                                };
11694                                return Err(ParseError {
11695                                    message,
11696                                    line: dialect_tok.line,
11697                                    column: dialect_tok.column,
11698                                    ..Default::default()
11699                                });
11700                            }
11701                            // Closing RParen.
11702                            let rparen_tok = self.current().clone();
11703                            if !self.check(TokenType::RParen) {
11704                                return Err(ParseError {
11705                                    message: format!(
11706                                        "Expected `)` after dialect name \
11707                                         in axonendpoint '{}' \
11708                                         (transport: sse(<dialect>) grammar).",
11709                                        node.name
11710                                    ),
11711                                    line: rparen_tok.line,
11712                                    column: rparen_tok.column,
11713                                    ..Default::default()
11714                                });
11715                            }
11716                            self.advance(); // consume RParen
11717                            node.transport_dialect = dialect;
11718                        }
11719                    }
11720                    "keepalive" => {
11721                        // Accepts either a DURATION token (e.g. `15s`) or
11722                        // an ident-like token. Validation against the
11723                        // closed enum {5s, 15s, 30s, 60s} happens after.
11724                        let value_tok = self.current().clone();
11725                        self.advance();
11726                        let value = &value_tok.value;
11727                        if !axonendpoint_is_valid_keepalive(value) {
11728                            let hint = crate::smart_suggest::suggest_for(
11729                                value,
11730                                AXONENDPOINT_KEEPALIVE_VALUES,
11731                            );
11732                            let base = format!(
11733                                "Invalid keepalive '{}' in axonendpoint '{}'.",
11734                                value, node.name
11735                            );
11736                            let message = if hint.is_empty() {
11737                                format!("{base} expected 5s | 15s | 30s | 60s, found {value}")
11738                            } else {
11739                                format!(
11740                                    "{base} {hint} (expected 5s | 15s | 30s | 60s, found {value})"
11741                                )
11742                            };
11743                            return Err(ParseError {
11744                                message,
11745                                line: value_tok.line,
11746                                column: value_tok.column,
11747                                ..Default::default()
11748                            });
11749                        }
11750                        node.keepalive = value.clone();
11751                    }
11752                    "backend" => {
11753                        // v1.31.0 (D2) — declared execution backend.
11754                        // Closed catalog `CANONICAL_PROVIDERS ∪ {auto,
11755                        // stub}`; an unknown name is a parse error with
11756                        // a smart-suggest hint (the same discipline as
11757                        // `method`/`transport`/`keepalive`). The
11758                        // type-checker re-validates defensively for
11759                        // ASTs built outside the parser (LSP, tests).
11760                        let value_tok = self.consume_any_ident_or_kw()?;
11761                        let value = &value_tok.value;
11762                        if !axonendpoint_is_valid_backend(value) {
11763                            let hint = crate::smart_suggest::suggest_for(
11764                                value,
11765                                AXONENDPOINT_BACKEND_VALUES,
11766                            );
11767                            let expected = AXONENDPOINT_BACKEND_VALUES.join(" | ");
11768                            let base = format!(
11769                                "Invalid backend '{}' in axonendpoint '{}'.",
11770                                value, node.name
11771                            );
11772                            let message = if hint.is_empty() {
11773                                format!("{base} expected {expected}, found {value}")
11774                            } else {
11775                                format!(
11776                                    "{base} {hint} (expected {expected}, found {value})"
11777                                )
11778                            };
11779                            return Err(ParseError {
11780                                message,
11781                                line: value_tok.line,
11782                                column: value_tok.column,
11783                                ..Default::default()
11784                            });
11785                        }
11786                        node.backend = value.clone();
11787                    }
11788                    _ => self.skip_value(),
11789                }
11790            } else if self.check(TokenType::LBrace) {
11791                self.skip_braced_block()?;
11792            }
11793        }
11794        self.consume(TokenType::RBrace)?;
11795        Ok(node)
11796    }
11797
11798    // ── Numeric helpers for Tier 2 field parsing ────────────────────
11799
11800    fn parse_optional_int(&mut self) -> Option<i64> {
11801        let tok = self.current().clone();
11802        match tok.ttype {
11803            TokenType::Integer => {
11804                self.advance();
11805                tok.value.parse::<i64>().ok()
11806            }
11807            _ => {
11808                self.advance();
11809                None
11810            }
11811        }
11812    }
11813
11814    fn parse_optional_float(&mut self) -> Option<f64> {
11815        let tok = self.current().clone();
11816        match tok.ttype {
11817            TokenType::Float | TokenType::Integer => {
11818                self.advance();
11819                tok.value.parse::<f64>().ok()
11820            }
11821            _ => {
11822                self.advance();
11823                None
11824            }
11825        }
11826    }
11827
11828    // ── LAMBDA DATA (ΛD) ──────────────────────────────────────────
11829
11830    fn parse_lambda_data(&mut self) -> Result<LambdaDataDefinition, ParseError> {
11831        let tok = self.consume(TokenType::Lambda)?;
11832        let name = self.consume(TokenType::Identifier)?;
11833        self.consume(TokenType::LBrace)?;
11834
11835        let mut node = LambdaDataDefinition {
11836            name: name.value.clone(),
11837            ontology: String::new(),
11838            certainty: 1.0,
11839            temporal_frame_start: String::new(),
11840            temporal_frame_end: String::new(),
11841            provenance: String::new(),
11842            derivation: String::new(),
11843            loc: Loc {
11844                line: tok.line,
11845                column: tok.column,
11846            },
11847            leading_trivia: Vec::new(),
11848            trailing_trivia: Vec::new(),
11849        };
11850
11851        while !self.check(TokenType::RBrace) {
11852            let field = self.current().clone();
11853            match field.ttype {
11854                TokenType::Ontology => {
11855                    self.advance();
11856                    self.consume(TokenType::Colon)?;
11857                    node.ontology = self.consume(TokenType::StringLit)?.value.clone();
11858                }
11859                TokenType::Certainty => {
11860                    self.advance();
11861                    self.consume(TokenType::Colon)?;
11862                    let val = self.current().clone();
11863                    match val.ttype {
11864                        TokenType::Float => {
11865                            self.advance();
11866                            node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
11867                        }
11868                        TokenType::Integer => {
11869                            self.advance();
11870                            node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
11871                        }
11872                        _ => {
11873                            return Err(ParseError {
11874                                message: format!(
11875                                    "Expected number for certainty, got '{}'",
11876                                    val.value
11877                                ),
11878                                line: val.line,
11879                                column: val.column,
11880                                                            ..Default::default()
11881                            });
11882                        }
11883                    }
11884                }
11885                TokenType::TemporalFrame => {
11886                    self.advance();
11887                    self.consume(TokenType::Colon)?;
11888                    node.temporal_frame_start = self.consume(TokenType::StringLit)?.value.clone();
11889                    // Optional second string for end frame
11890                    if self.check(TokenType::StringLit) {
11891                        node.temporal_frame_end = self.consume(TokenType::StringLit)?.value.clone();
11892                    }
11893                }
11894                TokenType::Provenance => {
11895                    self.advance();
11896                    self.consume(TokenType::Colon)?;
11897                    node.provenance = self.consume(TokenType::StringLit)?.value.clone();
11898                }
11899                TokenType::Derivation => {
11900                    self.advance();
11901                    self.consume(TokenType::Colon)?;
11902                    let d = self.current().clone();
11903                    self.advance();
11904                    node.derivation = d.value.clone();
11905                }
11906                _ => {
11907                    // Skip unknown fields gracefully
11908                    self.advance();
11909                    if self.check(TokenType::Colon) {
11910                        self.advance();
11911                        self.skip_value();
11912                    }
11913                }
11914            }
11915        }
11916
11917        self.consume(TokenType::RBrace)?;
11918        Ok(node)
11919    }
11920
11921    fn parse_lambda_data_apply(&mut self) -> Result<LambdaDataApplyNode, ParseError> {
11922        let tok = self.consume(TokenType::Lambda)?;
11923        let lambda_name = self.consume(TokenType::Identifier)?;
11924
11925        // Expect "on" keyword (parsed as identifier since it's not reserved)
11926        let on_tok = self.current().clone();
11927        self.advance();
11928        if on_tok.value != "on" {
11929            return Err(ParseError {
11930                message: format!(
11931                    "Expected 'on' after lambda data name in flow step, got '{}'",
11932                    on_tok.value
11933                ),
11934                line: on_tok.line,
11935                column: on_tok.column,
11936                            ..Default::default()
11937            });
11938        }
11939
11940        let target = self.current().clone();
11941        self.advance();
11942
11943        let mut output_type = String::new();
11944        if self.check(TokenType::Arrow) {
11945            self.advance();
11946            output_type = self.consume(TokenType::Identifier)?.value.clone();
11947        }
11948
11949        Ok(LambdaDataApplyNode {
11950            lambda_data_name: lambda_name.value.clone(),
11951            target: target.value.clone(),
11952            output_type,
11953            loc: Loc {
11954                line: tok.line,
11955                column: tok.column,
11956            },
11957        })
11958    }
11959
11960    // ── GENERIC (Tier 2+) ────────────────────────────────────────
11961
11962    fn parse_generic_declaration(&mut self) -> Result<Declaration, ParseError> {
11963        let kw_tok = self.current().clone();
11964        self.advance(); // consume keyword
11965
11966        // Try to consume a name (identifier or keyword-as-name)
11967        let name = if self.current().ttype == TokenType::Identifier {
11968            let n = self.current().value.clone();
11969            self.advance();
11970            n
11971        } else if !self.check(TokenType::LBrace)
11972            && !self.check(TokenType::LParen)
11973            && !self.check(TokenType::Eof)
11974            && self
11975                .current()
11976                .value
11977                .chars()
11978                .all(|c| c.is_alphanumeric() || c == '_')
11979        {
11980            let n = self.current().value.clone();
11981            self.advance();
11982            n
11983        } else {
11984            String::new()
11985        };
11986
11987        // Skip optional parens: (...)
11988        if self.check(TokenType::LParen) {
11989            self.advance();
11990            let mut depth = 1u32;
11991            while depth > 0 && !self.check(TokenType::Eof) {
11992                if self.check(TokenType::LParen) {
11993                    depth += 1;
11994                } else if self.check(TokenType::RParen) {
11995                    depth -= 1;
11996                }
11997                self.advance();
11998            }
11999        }
12000
12001        // Skip tokens until LBrace or next declaration
12002        while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
12003            if self.check(TokenType::Eof) {
12004                break;
12005            }
12006            self.advance();
12007        }
12008
12009        // Skip braced block if present
12010        if self.check(TokenType::LBrace) {
12011            self.skip_braced_block()?;
12012        }
12013
12014        Ok(Declaration::Generic(GenericDeclaration {
12015            keyword: kw_tok.value,
12016            name,
12017            loc: Loc {
12018                line: kw_tok.line,
12019                column: kw_tok.column,
12020            },
12021            leading_trivia: Vec::new(),
12022            trailing_trivia: Vec::new(),
12023        }))
12024    }
12025
12026    // ──────────────────────────────────────────────────────────────────
12027    // v1.6.0 — Mobile Typed Channels parsers
12028    // (paper_mobile_channels.md section 3 + plan/the design plan)
12029    //  Direct port of axon/compiler/parser.py:_parse_channel/emit/publish/discover.
12030    // ──────────────────────────────────────────────────────────────────
12031
12032    /// Parse: `channel Name { message, qos, lifetime, persistence, shield }`.
12033    fn parse_channel(&mut self) -> Result<ChannelDefinition, ParseError> {
12034        let tok = self.consume(TokenType::Channel)?;
12035        let name = self.consume(TokenType::Identifier)?.value;
12036        let mut node = ChannelDefinition {
12037            name: name.clone(),
12038            message: String::new(),
12039            qos: "at_least_once".to_string(),
12040            lifetime: "affine".to_string(),
12041            persistence: "ephemeral".to_string(),
12042            shield_ref: String::new(),
12043            loc: Loc {
12044                line: tok.line,
12045                column: tok.column,
12046            },
12047            leading_trivia: Vec::new(),
12048            trailing_trivia: Vec::new(),
12049        };
12050        self.consume(TokenType::LBrace)?;
12051        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
12052            let field_tok = self.current().clone();
12053            let field_name = field_tok.value.clone();
12054            self.advance();
12055            if !self.check(TokenType::Colon) {
12056                if self.check(TokenType::LBrace) {
12057                    self.skip_braced_block()?;
12058                }
12059                continue;
12060            }
12061            self.advance();
12062            match field_name.as_str() {
12063                "message" => node.message = self.parse_channel_message_type()?,
12064                "qos" => {
12065                    let q_tok = self.consume_any_ident_or_kw()?;
12066                    if !matches!(
12067                        q_tok.value.as_str(),
12068                        "at_most_once" | "at_least_once" | "exactly_once" | "broadcast" | "queue"
12069                    ) {
12070                        return Err(ParseError {
12071                            message: format!(
12072                                "Invalid qos '{}' in channel '{}' — \
12073                                 expected at_most_once | at_least_once | \
12074                                 exactly_once | broadcast | queue",
12075                                q_tok.value, name
12076                            ),
12077                            line: q_tok.line,
12078                            column: q_tok.column,
12079                                                    ..Default::default()
12080                        });
12081                    }
12082                    node.qos = q_tok.value;
12083                }
12084                "lifetime" => {
12085                    let lt_tok = self.consume_any_ident_or_kw()?;
12086                    if !matches!(lt_tok.value.as_str(), "linear" | "affine" | "persistent") {
12087                        return Err(ParseError {
12088                            message: format!(
12089                                "Invalid lifetime '{}' in channel '{}' — \
12090                                 expected linear | affine | persistent",
12091                                lt_tok.value, name
12092                            ),
12093                            line: lt_tok.line,
12094                            column: lt_tok.column,
12095                                                    ..Default::default()
12096                        });
12097                    }
12098                    node.lifetime = lt_tok.value;
12099                }
12100                "persistence" => {
12101                    let p_tok = self.consume_any_ident_or_kw()?;
12102                    if !matches!(p_tok.value.as_str(), "ephemeral" | "persistent_axonstore") {
12103                        return Err(ParseError {
12104                            message: format!(
12105                                "Invalid persistence '{}' in channel '{}' — \
12106                                 expected ephemeral | persistent_axonstore",
12107                                p_tok.value, name
12108                            ),
12109                            line: p_tok.line,
12110                            column: p_tok.column,
12111                                                    ..Default::default()
12112                        });
12113                    }
12114                    node.persistence = p_tok.value;
12115                }
12116                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
12117                _ => self.skip_value(),
12118            }
12119        }
12120        self.consume(TokenType::RBrace)?;
12121        Ok(node)
12122    }
12123
12124    /// Parse a `message:` value, supporting nested `Channel<…>`
12125    /// (second-order session types — paper section 3.3).
12126    fn parse_channel_message_type(&mut self) -> Result<String, ParseError> {
12127        let head = self.consume(TokenType::Identifier)?;
12128        let mut spelling = head.value;
12129        if self.check(TokenType::Lt) {
12130            self.advance();
12131            let inner = self.parse_channel_message_type()?;
12132            self.consume(TokenType::Gt)?;
12133            spelling = format!("{}<{}>", spelling, inner);
12134        }
12135        Ok(spelling)
12136    }
12137
12138    /// Parse: `emit ChannelName(value_ref)` — Chan-Output / Chan-Mobility.
12139    ///
12140    /// `value_ref` accepts a bare identifier (variable / channel name for
12141    /// mobility) or a dotted path (`Step.output.field`) referencing a prior
12142    /// step result (v1.6.0 — runtime resolves via ContextManager).
12143    fn parse_emit_step(&mut self) -> Result<FlowStep, ParseError> {
12144        let tok = self.consume(TokenType::Emit)?;
12145        let channel = self.consume(TokenType::Identifier)?.value;
12146        self.consume(TokenType::LParen)?;
12147        let value = self.parse_emit_value_ref()?;
12148        self.consume(TokenType::RParen)?;
12149        Ok(FlowStep::Emit(EmitStatement {
12150            channel_ref: channel,
12151            value_ref: value,
12152            loc: Loc {
12153                line: tok.line,
12154                column: tok.column,
12155            },
12156        }))
12157    }
12158
12159    /// v2.46.0 — parse `mint <Credential> as <binding>`. The credential
12160    /// reference must resolve to a declared `credential` (`axon-T895`,
12161    /// type-checker); the binding is a fresh flow-scoped name receiving the
12162    /// raw bearer string. Both tokens are required — a `mint` with no
12163    /// binding would mint authority into the void.
12164    fn parse_mint_step(&mut self) -> Result<FlowStep, ParseError> {
12165        let tok = self.consume(TokenType::Mint)?;
12166        let credential_ref = self.consume(TokenType::Identifier)?.value;
12167        self.consume(TokenType::As)?;
12168        let binding = self.consume(TokenType::Identifier)?.value;
12169        Ok(FlowStep::Mint(MintStep {
12170            credential_ref,
12171            binding,
12172            loc: Loc {
12173                line: tok.line,
12174                column: tok.column,
12175            },
12176        }))
12177    }
12178
12179    /// v2.48.0 — parse `rotate <SecretsStore> [where "<filter>"] with
12180    /// <Tool> as <binding>` (doctrine `rotation_without_revelation`).
12181    ///
12182    /// All three anchors are grammar, not convention: the store names WHAT
12183    /// may rotate (a `backend: secrets` class view — `axon-T898` in the
12184    /// type-checker), the tool names WHO performs the exchange
12185    /// (`axon-T899`), and the binding receives the metadata-only summary —
12186    /// a `rotate` without a binding would renew authority with no
12187    /// observable outcome, so `as` is REQUIRED (the `mint` posture). The
12188    /// `where` filter is optional (v2.21.0 string grammar, proven against the
12189    /// synthesized metadata schema); omitting it rotates the WHOLE class —
12190    /// the deliberate post-breach bulk shape. `with` is a soft keyword
12191    /// (not a lexer token): reserving it globally would break every
12192    /// adopter identifier named `with`.
12193    fn parse_rotate_step(&mut self) -> Result<FlowStep, ParseError> {
12194        let tok = self.consume(TokenType::Rotate)?;
12195        let store_ref = self.consume(TokenType::Identifier)?.value;
12196        let mut where_expr = String::new();
12197        if self.check(TokenType::Where) {
12198            self.advance();
12199            where_expr = self.consume(TokenType::StringLit)?.value.clone();
12200        }
12201        let with_tok = self.current().clone();
12202        if with_tok.value != "with" {
12203            return Err(ParseError {
12204                message: format!(
12205                    "Expected `with <Tool>` after `rotate {store_ref}{}`, found '{}'. \
12206                     A rotation names the tool that performs the renewal exchange: \
12207                     `rotate {store_ref} [where \"<filter>\"] with <Tool> as <binding>`.",
12208                    if where_expr.is_empty() { "" } else { " where …" },
12209                    with_tok.value
12210                ),
12211                line: with_tok.line,
12212                column: with_tok.column,
12213                ..Default::default()
12214            });
12215        }
12216        self.advance();
12217        let tool_ref = self.consume(TokenType::Identifier)?.value;
12218        self.consume(TokenType::As)?;
12219        let binding = self.consume(TokenType::Identifier)?.value;
12220        Ok(FlowStep::Rotate(RotateStep {
12221            store_ref,
12222            where_expr,
12223            tool_ref,
12224            binding,
12225            loc: Loc {
12226                line: tok.line,
12227                column: tok.column,
12228            },
12229        }))
12230    }
12231
12232    /// Parse: `IDENTIFIER ('.' (IDENTIFIER | keyword))*` → dot-joined string
12233    /// (v1.6.0).
12234    ///
12235    /// Mirrors the Python `_parse_emit_value_ref` helper exactly so the IR
12236    /// JSON for `emit Hello(Build.output)` is byte-identical between the
12237    /// two reference implementations.
12238    ///
12239    /// The HEAD must be a real ``Identifier``. Subsequent segments after a
12240    /// `.` may be identifiers OR keywords — common field names like
12241    /// ``output``, ``result``, ``message``, ``state``, etc. are reserved
12242    /// words in Axon but adopters must be able to write them as
12243    /// dotted-access segments. The accepting predicate:
12244    ///   - the lexer carried a non-empty `value` (every Word-like token does)
12245    ///   - the value's first byte is a letter or underscore (filters out
12246    ///     punctuation tokens such as ',', '{', etc.)
12247    fn parse_emit_value_ref(&mut self) -> Result<String, ParseError> {
12248        let head = self.consume(TokenType::Identifier)?.value;
12249        let mut parts = vec![head];
12250        while self.check(TokenType::Dot) {
12251            self.advance(); // consume '.'
12252            let next_tok = self.current().clone();
12253            let valid = !next_tok.value.is_empty()
12254                && next_tok.value.as_bytes()[0].is_ascii_alphabetic()
12255                || next_tok.value.starts_with('_');
12256            if !valid {
12257                return Err(ParseError {
12258                    message: format!(
12259                        "Expected identifier or keyword after '.' in dotted \
12260                         access, found {:?}",
12261                        next_tok.value
12262                    ),
12263                    line: next_tok.line,
12264                    column: next_tok.column,
12265                                    ..Default::default()
12266                });
12267            }
12268            self.advance();
12269            parts.push(next_tok.value);
12270        }
12271        Ok(parts.join("."))
12272    }
12273
12274    /// Parse: `publish ChannelName within ShieldName` — Publish-Ext (D8).
12275    fn parse_publish_step(&mut self) -> Result<FlowStep, ParseError> {
12276        let tok = self.consume(TokenType::Publish)?;
12277        let channel = self.consume(TokenType::Identifier)?.value;
12278        self.consume(TokenType::Within)?;
12279        let shield = self.consume(TokenType::Identifier)?.value;
12280        Ok(FlowStep::Publish(PublishStatement {
12281            channel_ref: channel,
12282            shield_ref: shield,
12283            loc: Loc {
12284                line: tok.line,
12285                column: tok.column,
12286            },
12287        }))
12288    }
12289
12290    /// Parse: `discover ChannelName as alias` — dual of publish.
12291    fn parse_discover_step(&mut self) -> Result<FlowStep, ParseError> {
12292        let tok = self.consume(TokenType::Discover)?;
12293        let cap = self.consume(TokenType::Identifier)?.value;
12294        self.consume(TokenType::As)?;
12295        let alias = self.consume(TokenType::Identifier)?.value;
12296        Ok(FlowStep::Discover(DiscoverStatement {
12297            capability_ref: cap,
12298            alias,
12299            loc: Loc {
12300                line: tok.line,
12301                column: tok.column,
12302            },
12303        }))
12304    }
12305}
12306
12307// ── v1.6.0 — Mobile Typed Channels parser tests ─────────────────────
12308
12309#[cfg(test)]
12310mod parser_tests {
12311    use super::*;
12312    use crate::lexer::Lexer;
12313
12314    fn parse(src: &str) -> Result<Program, ParseError> {
12315        let tokens = Lexer::new(src, "<test>").tokenize().expect("lex");
12316        Parser::new(tokens).parse()
12317    }
12318
12319    #[test]
12320    fn channel_full_parses() {
12321        let src = r#"channel C { message: Order qos: at_least_once lifetime: affine persistence: ephemeral shield: Gate }"#;
12322        let prog = parse(src).expect("parse");
12323        match &prog.declarations[0] {
12324            Declaration::Channel(c) => {
12325                assert_eq!(c.name, "C");
12326                assert_eq!(c.message, "Order");
12327                assert_eq!(c.qos, "at_least_once");
12328                assert_eq!(c.lifetime, "affine");
12329                assert_eq!(c.persistence, "ephemeral");
12330                assert_eq!(c.shield_ref, "Gate");
12331            }
12332            _ => panic!("expected ChannelDefinition"),
12333        }
12334    }
12335
12336    #[test]
12337    fn channel_defaults_match_paper_d1() {
12338        let prog = parse("channel C { message: Order }").expect("parse");
12339        if let Declaration::Channel(c) = &prog.declarations[0] {
12340            assert_eq!(c.qos, "at_least_once"); // default
12341            assert_eq!(c.lifetime, "affine"); // D1 default
12342            assert_eq!(c.persistence, "ephemeral");
12343            assert_eq!(c.shield_ref, "");
12344        } else {
12345            panic!("expected ChannelDefinition");
12346        }
12347    }
12348
12349    #[test]
12350    fn channel_second_order_message_type_parses() {
12351        let prog = parse("channel C { message: Channel<Order> }").expect("parse");
12352        if let Declaration::Channel(c) = &prog.declarations[0] {
12353            assert_eq!(c.message, "Channel<Order>");
12354        } else {
12355            panic!("expected ChannelDefinition");
12356        }
12357    }
12358
12359    #[test]
12360    fn channel_nested_channel_message_type_parses() {
12361        let prog = parse("channel C { message: Channel<Channel<Order>> }").expect("parse");
12362        if let Declaration::Channel(c) = &prog.declarations[0] {
12363            assert_eq!(c.message, "Channel<Channel<Order>>");
12364        } else {
12365            panic!("expected ChannelDefinition");
12366        }
12367    }
12368
12369    #[test]
12370    fn channel_invalid_qos_rejected() {
12371        let err = parse("channel C { message: T qos: bogus }").unwrap_err();
12372        assert!(err.message.contains("Invalid qos"), "got {}", err.message);
12373    }
12374
12375    #[test]
12376    fn channel_invalid_lifetime_rejected() {
12377        let err = parse("channel C { message: T lifetime: eternal }").unwrap_err();
12378        assert!(
12379            err.message.contains("Invalid lifetime"),
12380            "got {}",
12381            err.message
12382        );
12383    }
12384
12385    #[test]
12386    fn channel_invalid_persistence_rejected() {
12387        let err = parse("channel C { message: T persistence: forever }").unwrap_err();
12388        assert!(
12389            err.message.contains("Invalid persistence"),
12390            "got {}",
12391            err.message
12392        );
12393    }
12394
12395    #[test]
12396    fn emit_value_parses() {
12397        let src = "flow f() -> Out { emit C(payload) }";
12398        let prog = parse(src).expect("parse");
12399        if let Declaration::Flow(f) = &prog.declarations[0] {
12400            match &f.body[0] {
12401                FlowStep::Emit(e) => {
12402                    assert_eq!(e.channel_ref, "C");
12403                    assert_eq!(e.value_ref, "payload");
12404                }
12405                other => panic!("expected Emit, got {:?}", other),
12406            }
12407        } else {
12408            panic!("expected Flow");
12409        }
12410    }
12411
12412    #[test]
12413    fn publish_within_shield_parses() {
12414        let src = "flow f() -> Cap { publish C within Gate }";
12415        let prog = parse(src).expect("parse");
12416        if let Declaration::Flow(f) = &prog.declarations[0] {
12417            match &f.body[0] {
12418                FlowStep::Publish(p) => {
12419                    assert_eq!(p.channel_ref, "C");
12420                    assert_eq!(p.shield_ref, "Gate");
12421                }
12422                other => panic!("expected Publish, got {:?}", other),
12423            }
12424        } else {
12425            panic!("expected Flow");
12426        }
12427    }
12428
12429    #[test]
12430    fn discover_with_alias_parses() {
12431        let src = "flow f() -> Out { discover C as ch }";
12432        let prog = parse(src).expect("parse");
12433        if let Declaration::Flow(f) = &prog.declarations[0] {
12434            match &f.body[0] {
12435                FlowStep::Discover(d) => {
12436                    assert_eq!(d.capability_ref, "C");
12437                    assert_eq!(d.alias, "ch");
12438                }
12439                other => panic!("expected Discover, got {:?}", other),
12440            }
12441        } else {
12442            panic!("expected Flow");
12443        }
12444    }
12445
12446    #[test]
12447    fn listen_typed_ref_sets_flag_true() {
12448        let src = "daemon D() { goal: \"x\" listen C as ev { } }";
12449        let prog = parse(src).expect("parse");
12450        if let Declaration::Daemon(d) = &prog.declarations[0] {
12451            assert_eq!(d.listeners.len(), 1);
12452            assert_eq!(d.listeners[0].channel, "C");
12453            assert!(d.listeners[0].channel_is_ref, "typed ref ⇒ true");
12454        } else {
12455            panic!("expected Daemon");
12456        }
12457    }
12458
12459    #[test]
12460    fn listen_string_topic_legacy_flag_false() {
12461        let src = "daemon D() { goal: \"x\" listen \"orders\" as ev { } }";
12462        let prog = parse(src).expect("parse");
12463        if let Declaration::Daemon(d) = &prog.declarations[0] {
12464            assert_eq!(d.listeners.len(), 1);
12465            assert_eq!(d.listeners[0].channel, "orders");
12466            assert!(!d.listeners[0].channel_is_ref, "string topic ⇒ false");
12467        } else {
12468            panic!("expected Daemon");
12469        }
12470    }
12471
12472    // ── v1.6.0 — emit value_ref accepts dotted access ───────────
12473
12474    fn extract_first_emit(prog: &Program) -> &EmitStatement {
12475        if let Declaration::Flow(f) = &prog.declarations[0] {
12476            if let FlowStep::Emit(e) = &f.body[0] {
12477                return e;
12478            }
12479        }
12480        panic!("expected emit statement at flow body[0]");
12481    }
12482
12483    #[test]
12484    fn emit_accepts_bare_identifier_value_ref() {
12485        // Pre-13.i baseline — must keep working.
12486        let prog = parse("flow f() -> Out { emit Hello(payload) }").expect("parse");
12487        let emit = extract_first_emit(&prog);
12488        assert_eq!(emit.channel_ref, "Hello");
12489        assert_eq!(emit.value_ref, "payload");
12490    }
12491
12492    #[test]
12493    fn emit_accepts_two_segment_dotted_value_ref() {
12494        // The exact case adopters reported as broken before 13.i.
12495        let prog = parse("flow f() -> Out { emit Hello(Build.output) }").expect("parse");
12496        let emit = extract_first_emit(&prog);
12497        assert_eq!(emit.value_ref, "Build.output");
12498    }
12499
12500    #[test]
12501    fn emit_accepts_three_segment_nested_dotted_value_ref() {
12502        let prog = parse("flow f() -> Out { emit Score(Analyze.result.score) }").expect("parse");
12503        let emit = extract_first_emit(&prog);
12504        assert_eq!(emit.value_ref, "Analyze.result.score");
12505    }
12506
12507    #[test]
12508    fn emit_dotted_with_trailing_dot_fails() {
12509        // Trailing `.` must still error — every '.' demands an identifier.
12510        let result = parse("flow f() -> Out { emit Hello(Build.) }");
12511        assert!(result.is_err(), "expected parse error for trailing dot");
12512    }
12513}
12514
12515// ── v1.5.2 — declaration_trivia parallel channel tests ──────────────────
12516
12517#[cfg(test)]
12518mod declaration_trivia_tests {
12519    use super::*;
12520    use crate::lexer::Lexer;
12521    use crate::tokens::TriviaKind;
12522
12523    fn parse(src: &str) -> Program {
12524        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
12525        Parser::new(toks).parse().expect("parse")
12526    }
12527
12528    #[test]
12529    fn no_comments_means_empty_trivia_per_decl() {
12530        let prog = parse("flow F() -> Out { }");
12531        assert_eq!(prog.declarations.len(), 1);
12532        assert_eq!(prog.declaration_trivia.len(), 1);
12533        assert!(prog.declaration_trivia[0].leading.is_empty());
12534        assert!(prog.declaration_trivia[0].trailing.is_empty());
12535    }
12536
12537    #[test]
12538    fn doc_line_comment_attaches_as_leading() {
12539        let prog = parse("/// Documents F\nflow F() -> Out { }");
12540        let triv = &prog.declaration_trivia[0];
12541        assert_eq!(triv.leading.len(), 1);
12542        assert_eq!(triv.leading[0].kind, TriviaKind::DocLine);
12543        assert!(triv.leading[0].is_doc());
12544        assert_eq!(triv.leading[0].text, "/// Documents F");
12545    }
12546
12547    #[test]
12548    fn regular_line_comment_attaches_as_leading() {
12549        let prog = parse("// header\nflow F() -> Out { }");
12550        let triv = &prog.declaration_trivia[0];
12551        assert_eq!(triv.leading.len(), 1);
12552        assert_eq!(triv.leading[0].kind, TriviaKind::Line);
12553        assert!(!triv.leading[0].is_doc());
12554    }
12555
12556    #[test]
12557    fn block_doc_comment_attaches_as_leading() {
12558        let prog = parse("/** Doc block */\nflow F() -> Out { }");
12559        let triv = &prog.declaration_trivia[0];
12560        assert_eq!(triv.leading[0].kind, TriviaKind::DocBlock);
12561        assert!(triv.leading[0].is_doc());
12562    }
12563
12564    #[test]
12565    fn multiple_comments_collected_in_source_order() {
12566        let src = "/// First\n/// Second\nflow F() -> Out { }";
12567        let prog = parse(src);
12568        let triv = &prog.declaration_trivia[0];
12569        assert_eq!(triv.leading.len(), 2);
12570        assert_eq!(triv.leading[0].text, "/// First");
12571        assert_eq!(triv.leading[1].text, "/// Second");
12572    }
12573
12574    #[test]
12575    fn three_decls_each_get_own_leading() {
12576        let src = "/// for A\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }\n/// for C\nflow C() -> Out { }";
12577        let prog = parse(src);
12578        assert_eq!(prog.declarations.len(), 3);
12579        assert_eq!(prog.declaration_trivia.len(), 3);
12580        for (idx, name) in ["A", "B", "C"].iter().enumerate() {
12581            let triv = &prog.declaration_trivia[idx];
12582            assert_eq!(triv.leading.len(), 1);
12583            assert_eq!(triv.leading[0].text, format!("/// for {name}"));
12584        }
12585    }
12586
12587    #[test]
12588    fn trailing_comment_attaches_to_last_token_of_decl() {
12589        // Comment on the same line as the decl's closing brace.
12590        let prog = parse("flow F() -> Out { } // tail");
12591        let triv = &prog.declaration_trivia[0];
12592        assert_eq!(triv.trailing.len(), 1);
12593        assert_eq!(triv.trailing[0].text, "// tail");
12594    }
12595
12596    #[test]
12597    fn mixed_doc_and_regular_preserve_order_between_decls() {
12598        let src = "/// doc for A\nflow A() -> Out { }\n\n// header line\n/// doc for B\nflow B() -> Out { }";
12599        let prog = parse(src);
12600        assert_eq!(prog.declarations.len(), 2);
12601        // A: just the doc comment.
12602        assert_eq!(prog.declaration_trivia[0].leading.len(), 1);
12603        // B: header + doc, in source order.
12604        assert_eq!(prog.declaration_trivia[1].leading.len(), 2);
12605        assert_eq!(prog.declaration_trivia[1].leading[0].text, "// header line");
12606        assert_eq!(prog.declaration_trivia[1].leading[1].text, "/// doc for B");
12607    }
12608
12609    #[test]
12610    fn parser_unaffected_by_comments_in_grammar_path() {
12611        // The parser must accept comments interleaved between every
12612        // legal token without affecting the AST shape it produces.
12613        // This is the regression guard for "lossless lexing must not
12614        // change parsing semantics."
12615        let src =
12616            "// before flow\nflow /* between flow and name */ F() -> Out {\n  // body comment\n}";
12617        let prog = parse(src);
12618        assert_eq!(prog.declarations.len(), 1);
12619        if let Declaration::Flow(f) = &prog.declarations[0] {
12620            assert_eq!(f.name, "F");
12621        } else {
12622            panic!("expected Flow declaration");
12623        }
12624    }
12625}
12626
12627// ── v1.5.2 — per-struct trivia fields tests ─────────────────────────────
12628//
12629// 14.b spreads `leading_trivia` / `trailing_trivia` into every Declaration
12630// variant struct (FlowDefinition, ChannelDefinition, PersonaDefinition, …).
12631// The Python AST already had this shape since 14.a; 14.b achieves Rust
12632// parity. The side-channel `Program.declaration_trivia` is preserved for
12633// backward compat — these tests verify the new direct access path.
12634
12635#[cfg(test)]
12636mod per_struct_trivia_tests {
12637    use super::*;
12638    use crate::lexer::Lexer;
12639    use crate::tokens::TriviaKind;
12640
12641    fn parse(src: &str) -> Program {
12642        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
12643        Parser::new(toks).parse().expect("parse")
12644    }
12645
12646    #[test]
12647    fn flow_definition_carries_leading_trivia_directly() {
12648        let prog = parse("/// documents F\nflow F() -> Out { }");
12649        if let Declaration::Flow(f) = &prog.declarations[0] {
12650            assert_eq!(f.leading_trivia.len(), 1);
12651            assert_eq!(f.leading_trivia[0].kind, TriviaKind::DocLine);
12652            assert_eq!(f.leading_trivia[0].text, "/// documents F");
12653            assert!(f.trailing_trivia.is_empty());
12654        } else {
12655            panic!("expected Flow declaration");
12656        }
12657    }
12658
12659    #[test]
12660    fn flow_definition_carries_trailing_trivia_directly() {
12661        let prog = parse("flow F() -> Out { } // tail comment");
12662        if let Declaration::Flow(f) = &prog.declarations[0] {
12663            assert_eq!(f.trailing_trivia.len(), 1);
12664            assert_eq!(f.trailing_trivia[0].text, "// tail comment");
12665        } else {
12666            panic!("expected Flow declaration");
12667        }
12668    }
12669
12670    #[test]
12671    fn channel_definition_carries_trivia_directly() {
12672        // ChannelDefinition is a Tier-1 declaration; verify per-struct fields
12673        // populate just like FlowDefinition.
12674        let src = concat!(
12675            "/// inbound order events\n",
12676            "channel Orders {\n",
12677            "    message:     Order\n",
12678            "    qos:         at_least_once\n",
12679            "    lifetime:    affine\n",
12680            "    persistence: ephemeral\n",
12681            "    shield:      Broker\n",
12682            "}",
12683        );
12684        let prog = parse(src);
12685        if let Declaration::Channel(ch) = &prog.declarations[0] {
12686            assert_eq!(ch.leading_trivia.len(), 1);
12687            assert!(ch.leading_trivia[0].is_doc());
12688            assert_eq!(ch.leading_trivia[0].text, "/// inbound order events");
12689        } else {
12690            panic!("expected Channel declaration");
12691        }
12692    }
12693
12694    #[test]
12695    fn per_struct_fields_match_side_channel() {
12696        // 14.a side-channel and 14.b per-struct fields must hold identical
12697        // data — they are populated by the same parser pass.
12698        let src = "/// for A\n// header for B\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }";
12699        let prog = parse(src);
12700        for (idx, decl) in prog.declarations.iter().enumerate() {
12701            let side = &prog.declaration_trivia[idx];
12702            let (per_lead, per_trail) = match decl {
12703                Declaration::Flow(f) => (&f.leading_trivia, &f.trailing_trivia),
12704                _ => panic!("unexpected variant"),
12705            };
12706            assert_eq!(per_lead.len(), side.leading.len());
12707            assert_eq!(per_trail.len(), side.trailing.len());
12708            for (a, b) in per_lead.iter().zip(side.leading.iter()) {
12709                assert_eq!(a.text, b.text);
12710                assert_eq!(a.kind, b.kind);
12711            }
12712        }
12713    }
12714
12715    #[test]
12716    fn comment_free_program_yields_empty_per_struct_fields() {
12717        let prog = parse("flow F() -> Out { }");
12718        if let Declaration::Flow(f) = &prog.declarations[0] {
12719            assert!(f.leading_trivia.is_empty());
12720            assert!(f.trailing_trivia.is_empty());
12721        } else {
12722            panic!("expected Flow declaration");
12723        }
12724    }
12725}
12726
12727// ── v1.5.2 — inner doc comments (//!, /*!) ──────────────────────────────
12728//
12729// Inner doc comments document the *enclosing* item rather than the next
12730// sibling. Today they flow through the trivia channel like any other
12731// comment; downstream consumers (axon doc, LSP) decide how to interpret
12732// `is_inner_doc()`. These tests verify the lexer→parser pipeline preserves
12733// the inner-doc discriminator end-to-end.
12734
12735#[cfg(test)]
12736mod inner_doc_tests {
12737    use super::*;
12738    use crate::lexer::Lexer;
12739    use crate::tokens::TriviaKind;
12740
12741    fn parse(src: &str) -> Program {
12742        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
12743        Parser::new(toks).parse().expect("parse")
12744    }
12745
12746    #[test]
12747    fn inner_doc_line_reaches_leading_trivia() {
12748        let src = "//! file-level docs\nflow F() -> Out { }";
12749        let prog = parse(src);
12750        let triv = &prog.declaration_trivia[0];
12751        assert_eq!(triv.leading.len(), 1);
12752        assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocLine);
12753        assert!(triv.leading[0].is_doc());
12754        assert!(triv.leading[0].is_inner_doc());
12755        assert_eq!(triv.leading[0].text, "//! file-level docs");
12756        assert_eq!(triv.leading[0].stripped_text(), " file-level docs");
12757    }
12758
12759    #[test]
12760    fn inner_doc_block_reaches_leading_trivia() {
12761        let src = "/*! module-level docs */\nflow F() -> Out { }";
12762        let prog = parse(src);
12763        let triv = &prog.declaration_trivia[0];
12764        assert_eq!(triv.leading.len(), 1);
12765        assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocBlock);
12766        assert!(triv.leading[0].is_inner_doc());
12767        assert_eq!(triv.leading[0].stripped_text(), " module-level docs ");
12768    }
12769
12770    #[test]
12771    fn outer_and_inner_doc_can_coexist() {
12772        // File-level inner doc on top, then an outer doc for the
12773        // declaration. Both reach the trivia channel and remain
12774        // distinguishable via `is_inner_doc()`.
12775        let src = "//! file docs\n/// docs F\nflow F() -> Out { }";
12776        let prog = parse(src);
12777        let triv = &prog.declaration_trivia[0];
12778        assert_eq!(triv.leading.len(), 2);
12779        assert!(triv.leading[0].is_inner_doc());
12780        assert!(triv.leading[1].is_doc());
12781        assert!(!triv.leading[1].is_inner_doc());
12782    }
12783
12784    #[test]
12785    fn inner_doc_reaches_per_struct_fields() {
12786        // Same data must be visible via the per-struct fields (v1.5.2).
12787        let src = "//! intro\nflow F() -> Out { }";
12788        let prog = parse(src);
12789        if let Declaration::Flow(f) = &prog.declarations[0] {
12790            assert_eq!(f.leading_trivia.len(), 1);
12791            assert!(f.leading_trivia[0].is_inner_doc());
12792        } else {
12793            panic!("expected Flow declaration");
12794        }
12795    }
12796}
12797
12798// ── v1.20.0 — Parser error recovery test pack ─────────────────────────────
12799//
12800// Mirror of `tests/test_fase28_parser_recovery.py` (Python side, 28.b).
12801// The test classes here line up 1-1 with the Python ones so the cross-
12802// stack drift gate (28.i) can compare error-list shapes input-for-input.
12803//
12804// Test classes:
12805//   - backwards_compat: existing `parse()` API unchanged
12806//   - single_error_recovery: one bad decl → one error, rest parse OK
12807//   - multi_error_recovery: N independent errors → N entries
12808//   - sync_points: every top-level keyword resyncs correctly
12809//   - parse_result_api: `has_errors`, `is_clean`
12810//   - edge_cases: EOF mid-error, brace imbalance, only-bad-tokens
12811//   - robustness_fuzz: 1000 deterministic-seeded mutations never crash
12812//   - no_ghost_errors: single broken field produces exactly 1 error
12813//   - integration_with_colon_diagnostic: v1.19.4 hint preserved under
12814//     recovery mode
12815#[cfg(test)]
12816mod recovery_tests {
12817    use super::*;
12818    use crate::lexer::Lexer;
12819
12820    /// Lex a source and return tokens for the parser to consume.
12821    /// Mirrors the Python `_parse_recovery` helper.
12822    fn lex(src: &str) -> Vec<Token> {
12823        Lexer::new(src, "<test>").tokenize().expect("lex")
12824    }
12825
12826    /// Parse with recovery mode. Returns `(program, errors)` so call
12827    /// sites read like the Python helper.
12828    fn recover(src: &str) -> ParseResult {
12829        Parser::new(lex(src)).parse_with_recovery()
12830    }
12831
12832    /// Strict parse. Mirrors the Python `_parse_strict` helper.
12833    fn strict(src: &str) -> Result<Program, ParseError> {
12834        Parser::new(lex(src)).parse()
12835    }
12836
12837    // ── backwards_compat ─────────────────────────────────────────
12838
12839    #[test]
12840    fn strict_parse_unchanged_for_clean_source() {
12841        // The existing `parse()` API must continue to succeed
12842        // verbatim on every well-formed input — D9.
12843        let src = "intent I {}";
12844        let prog = strict(src).expect("clean parse");
12845        assert_eq!(prog.declarations.len(), 1);
12846    }
12847
12848    #[test]
12849    fn strict_parse_still_raises_on_first_error() {
12850        // D9 + D8: opt-in to recovery via `parse_with_recovery`;
12851        // strict mode must still bubble the first error.
12852        // (Using a parse-time error rather than a lex error — `@@@`
12853        // would be rejected by the lexer, which is out of scope.)
12854        let src = "flow F() { } not_a_keyword flow G() { }";
12855        let _ = strict(src).expect_err("must error fast in strict mode");
12856    }
12857
12858    #[test]
12859    fn recovery_clean_source_yields_no_errors() {
12860        let src = "flow F() { } flow G() { }";
12861        let pr = recover(src);
12862        assert!(pr.is_clean(), "errors: {:?}", pr.errors);
12863        assert_eq!(pr.program.declarations.len(), 2);
12864    }
12865
12866    // ── single_error_recovery ────────────────────────────────────
12867
12868    #[test]
12869    fn single_unknown_top_level_token_recovers() {
12870        // One garbage token at top level; rest must parse.
12871        let src = "garbage_token flow F() { } flow G() { }";
12872        let pr = recover(src);
12873        assert_eq!(pr.errors.len(), 1, "errors: {:?}", pr.errors);
12874        assert_eq!(pr.program.declarations.len(), 2);
12875    }
12876
12877    #[test]
12878    fn error_in_first_decl_does_not_block_second() {
12879        // `flow F` body refers to non-keyword `nope`; the error
12880        // recovery must skip to the next top-level keyword.
12881        let src = "flow F() { not_a_step nope } flow G() { }";
12882        let pr = recover(src);
12883        assert!(pr.has_errors(), "expected at least one error");
12884        // The second flow must be reachable.
12885        let names: Vec<&str> = pr
12886            .program
12887            .declarations
12888            .iter()
12889            .filter_map(|d| match d {
12890                Declaration::Flow(f) => Some(f.name.as_str()),
12891                _ => None,
12892            })
12893            .collect();
12894        assert!(names.contains(&"G"), "G not found among {names:?}");
12895    }
12896
12897    #[test]
12898    fn malformed_declaration_then_clean_intent_recovers() {
12899        let src = "flow @ () { } intent I {}";
12900        let pr = recover(src);
12901        assert!(pr.has_errors());
12902        let kinds: Vec<&str> = pr
12903            .program
12904            .declarations
12905            .iter()
12906            .map(|d| match d {
12907                Declaration::Intent(_) => "intent",
12908                Declaration::Flow(_) => "flow",
12909                _ => "other",
12910            })
12911            .collect();
12912        assert!(kinds.contains(&"intent"), "kinds: {kinds:?}");
12913    }
12914
12915    #[test]
12916    fn recovery_does_not_double_count_a_single_error() {
12917        // Regression for the "ghost error" pathology that surfaced
12918        // during 28.b dev: a nested-decl error must not also fire
12919        // an "Unexpected token at top level" from the outer loop.
12920        // The Rust grammar has stricter intra-flow requirements
12921        // than Python; the invariant we assert here is that the
12922        // outer loop emits zero "Unexpected token at top level"
12923        // errors after an inner step-shape error.
12924        let src = "flow F() { not_a_step }";
12925        let pr = recover(src);
12926        let outer_ghosts = pr
12927            .errors
12928            .iter()
12929            .filter(|e| e.message.contains("at top level"))
12930            .count();
12931        assert_eq!(outer_ghosts, 0, "ghost errors: {:?}", pr.errors);
12932    }
12933
12934    // ── multi_error_recovery ─────────────────────────────────────
12935
12936    #[test]
12937    fn three_independent_errors_yield_three_entries() {
12938        let src =
12939            "garbage1 flow F() { } garbage2 flow G() { } garbage3 flow H() { }";
12940        let pr = recover(src);
12941        assert_eq!(pr.errors.len(), 3, "errors: {:?}", pr.errors);
12942        assert_eq!(pr.program.declarations.len(), 3);
12943    }
12944
12945    #[test]
12946    fn all_errors_no_valid_declarations() {
12947        let src = "foo bar baz qux";
12948        let pr = recover(src);
12949        assert!(pr.has_errors());
12950        assert!(pr.program.declarations.is_empty());
12951    }
12952
12953    #[test]
12954    fn errors_recorded_in_source_order() {
12955        let src = "x flow A() { } y flow B() { } z flow C() { }";
12956        let pr = recover(src);
12957        assert_eq!(pr.errors.len(), 3);
12958        let lines: Vec<u32> = pr.errors.iter().map(|e| e.line).collect();
12959        // Same source-line means we compare by column ordering;
12960        // either way they must be non-decreasing.
12961        assert!(
12962            lines.windows(2).all(|w| w[0] <= w[1]),
12963            "errors out of order: {lines:?}"
12964        );
12965    }
12966
12967    // ── sync_points ──────────────────────────────────────────────
12968
12969    #[test]
12970    fn sync_to_flow_keyword() {
12971        let src = "garbage flow F() { }";
12972        let pr = recover(src);
12973        assert_eq!(pr.program.declarations.len(), 1);
12974    }
12975
12976    #[test]
12977    fn sync_to_intent_keyword() {
12978        let src = "garbage intent I {}";
12979        let pr = recover(src);
12980        assert_eq!(pr.program.declarations.len(), 1);
12981    }
12982
12983    #[test]
12984    fn sync_to_persona_keyword() {
12985        let src = "garbage persona P { name: \"P\" role: \"R\" }";
12986        let pr = recover(src);
12987        assert!(
12988            pr.program
12989                .declarations
12990                .iter()
12991                .any(|d| matches!(d, Declaration::Persona(_))),
12992            "persona not recovered: decls = {:?}",
12993            pr.program.declarations.len()
12994        );
12995    }
12996
12997    #[test]
12998    fn sync_to_run_keyword() {
12999        let src = "garbage run R { input: { user_message: \"hi\" } }";
13000        let pr = recover(src);
13001        // Either Run was parsed, or recovery still produced ≥1 err.
13002        assert!(pr.has_errors());
13003    }
13004
13005    // ── parse_result_api ─────────────────────────────────────────
13006
13007    #[test]
13008    fn parse_result_has_errors_and_is_clean_invert() {
13009        let pr_clean = recover("flow F() { }");
13010        assert!(pr_clean.is_clean());
13011        assert!(!pr_clean.has_errors());
13012
13013        let pr_err = recover("garbage");
13014        assert!(!pr_err.is_clean());
13015        assert!(pr_err.has_errors());
13016    }
13017
13018    #[test]
13019    fn parse_result_program_field_holds_partial_program() {
13020        let pr = recover("garbage flow F() { }");
13021        assert!(!pr.program.declarations.is_empty());
13022    }
13023
13024    #[test]
13025    fn parse_result_errors_carry_line_and_column() {
13026        let pr = recover("garbage");
13027        assert!(!pr.errors.is_empty());
13028        let e = &pr.errors[0];
13029        assert!(e.line >= 1);
13030        // Column may be 0-based or 1-based depending on lexer;
13031        // accept anything ≥ 0.
13032        let _ = e.column;
13033        assert!(!e.message.is_empty());
13034    }
13035
13036    #[test]
13037    fn parse_result_debug_renders() {
13038        let pr = recover("flow F() { }");
13039        let s = format!("{pr:?}");
13040        assert!(s.contains("ParseResult"));
13041    }
13042
13043    // ── edge_cases ───────────────────────────────────────────────
13044
13045    #[test]
13046    fn empty_source_is_clean() {
13047        let pr = recover("");
13048        assert!(pr.is_clean());
13049        assert!(pr.program.declarations.is_empty());
13050    }
13051
13052    #[test]
13053    fn whitespace_only_source_is_clean() {
13054        let pr = recover("   \n\n\t  \n");
13055        assert!(pr.is_clean());
13056        assert!(pr.program.declarations.is_empty());
13057    }
13058
13059    #[test]
13060    fn only_garbage_does_not_crash() {
13061        // Lex-clean garbage tokens (avoids AxonLexerError).
13062        let pr = recover("foo bar baz { qux quux } corge { grault }");
13063        assert!(pr.has_errors());
13064    }
13065
13066    #[test]
13067    fn unbalanced_close_brace_does_not_crash() {
13068        let pr = recover("} flow F() { }");
13069        // Recovery must keep walking past stray `}`.
13070        let names: Vec<&str> = pr
13071            .program
13072            .declarations
13073            .iter()
13074            .filter_map(|d| match d {
13075                Declaration::Flow(f) => Some(f.name.as_str()),
13076                _ => None,
13077            })
13078            .collect();
13079        assert!(names.contains(&"F"), "F not recovered: {names:?}");
13080    }
13081
13082    #[test]
13083    fn error_at_eof_does_not_loop() {
13084        // Truncated declaration. Must terminate; finite errors.
13085        let pr = recover("flow F() { ");
13086        // Either errored or somehow accepted — but must terminate.
13087        let _ = pr.errors.len();
13088    }
13089
13090    #[test]
13091    fn nested_braces_inside_error_still_balance() {
13092        // Walker must respect brace depth so a `}` inside a malformed
13093        // block does not prematurely sync.
13094        let src = "flow F() { not_a_step { inner } } flow G() { }";
13095        let pr = recover(src);
13096        let names: Vec<&str> = pr
13097            .program
13098            .declarations
13099            .iter()
13100            .filter_map(|d| match d {
13101                Declaration::Flow(f) => Some(f.name.as_str()),
13102                _ => None,
13103            })
13104            .collect();
13105        assert!(names.contains(&"G"), "G not recovered: {names:?}");
13106    }
13107
13108    // ── robustness_fuzz ──────────────────────────────────────────
13109    //
13110    // Deterministic-seeded mutator (xorshift). 100 buckets ×
13111    // 10 mutations = 1000 iterations, byte-bounded so fuzz time
13112    // stays under 1 s on a release build. Recovery must NEVER crash;
13113    // lexer-level errors are out of scope (lexer recovery is its own
13114    // step). 28.b mirrors this with the same structure.
13115
13116    #[derive(Clone, Copy)]
13117    struct Xorshift(u64);
13118    impl Xorshift {
13119        fn next(&mut self) -> u64 {
13120            let mut x = self.0;
13121            x ^= x << 13;
13122            x ^= x >> 7;
13123            x ^= x << 17;
13124            self.0 = x;
13125            x
13126        }
13127        fn pick<T: Copy>(&mut self, slice: &[T]) -> T {
13128            slice[(self.next() as usize) % slice.len()]
13129        }
13130    }
13131
13132    fn mutate(src: &str, rng: &mut Xorshift) -> String {
13133        let mut bytes: Vec<u8> = src.bytes().collect();
13134        if bytes.is_empty() {
13135            return src.to_string();
13136        }
13137        let op = rng.next() % 4;
13138        let pos = (rng.next() as usize) % bytes.len();
13139        // Stick to ASCII-safe printable bytes to keep input lex-able
13140        // most of the time. AxonLexerError is still possible and is
13141        // tolerated by the recovery contract.
13142        let safe: &[u8] = b"abcdefghijklmnopqrstuvwxyz {}();:,_0123456789";
13143        match op {
13144            0 => {
13145                bytes.remove(pos);
13146            }
13147            1 => {
13148                let b = rng.pick(safe);
13149                bytes.insert(pos, b);
13150            }
13151            2 if pos + 1 < bytes.len() => {
13152                bytes.swap(pos, pos + 1);
13153            }
13154            _ => {
13155                let b = rng.pick(safe);
13156                bytes[pos] = b;
13157            }
13158        }
13159        // Lossy decode: mutator may have produced invalid UTF-8;
13160        // strip non-ASCII before handing to the lexer.
13161        bytes.retain(|b| b.is_ascii());
13162        String::from_utf8_lossy(&bytes).into_owned()
13163    }
13164
13165    #[test]
13166    fn fuzz_recovery_never_crashes() {
13167        let seed_bases = [
13168            "flow F() { }",
13169            "intent I { }",
13170            "persona P { name: \"P\" role: \"R\" }",
13171            "intent J { ask: \"a\" }",
13172            "type T = String",
13173        ];
13174        // 100 buckets × 10 mutations = 1000 iterations, deterministic.
13175        for (bucket, base) in (0..100u64).zip(seed_bases.iter().cycle()) {
13176            let mut rng = Xorshift(0x1234_5678_9abc_def0_u64.wrapping_add(bucket));
13177            let mut current = (*base).to_string();
13178            for _ in 0..10 {
13179                current = mutate(&current, &mut rng);
13180                // Lexer may reject; that's outside parser-recovery
13181                // scope (28.b/c). Skip those iterations.
13182                let toks = match Lexer::new(&current, "<fuzz>").tokenize() {
13183                    Ok(t) => t,
13184                    Err(_) => continue,
13185                };
13186                // Recovery must not panic on any well-lexed input.
13187                let _pr = Parser::new(toks).parse_with_recovery();
13188            }
13189        }
13190    }
13191
13192    // ── integration_with_v1_19_4_colon_diagnostic ────────────────
13193
13194    #[test]
13195    fn missing_colon_hint_preserved_under_recovery() {
13196        // The Rust frontend's strict `parse()` carries the same
13197        // colon diagnostic shape as the Python side. Recovery mode
13198        // must not erase it.
13199        let src = "flow F() { run R { input { user_message: \"hi\" } } }";
13200        let pr = recover(src);
13201        // Either the parser accepts this (some shape may be valid)
13202        // or it errors — but if it errors, the message must surface
13203        // the diagnostic content.
13204        if !pr.errors.is_empty() {
13205            let any_msg = pr.errors.iter().any(|e| !e.message.is_empty());
13206            assert!(any_msg);
13207        }
13208    }
13209
13210    // ── recovery preserves declaration ordering ──────────────────
13211
13212    #[test]
13213    fn recovered_declarations_appear_in_source_order() {
13214        let src = "flow A() { } garbage flow B() { } garbage flow C() { }";
13215        let pr = recover(src);
13216        let names: Vec<&str> = pr
13217            .program
13218            .declarations
13219            .iter()
13220            .filter_map(|d| match d {
13221                Declaration::Flow(f) => Some(f.name.as_str()),
13222                _ => None,
13223            })
13224            .collect();
13225        assert_eq!(names, vec!["A", "B", "C"]);
13226    }
13227}
13228
13229// ── v1.20.0 — Source-context diagnostic block test pack ───────────────────
13230//
13231// Mirror of `tests/test_fase28_source_context.py` (Python side, 28.d).
13232// The render output must be byte-identical to the Python `SourceSnippet.render`
13233// on the same input — D7 ratified (cross-stack drift gate). Golden strings
13234// in `golden_*` tests are duplicated verbatim in the Python pack; edits
13235// here MUST be mirrored on the Python side and vice versa.
13236#[cfg(test)]
13237mod source_context_tests {
13238    use super::*;
13239    use crate::lexer::Lexer;
13240
13241    fn snippet(source: &str, line: u32, column: u32, filename: &str) -> String {
13242        SourceSnippet::new(
13243            source.to_string(),
13244            line,
13245            column,
13246            filename.to_string(),
13247        )
13248        .render()
13249    }
13250
13251    // ── Pure rendering ──────────────────────────────────────────
13252
13253    #[test]
13254    fn rustc_style_block_for_middle_line() {
13255        let src = "line one\nline two\nline three\nline four\nline five";
13256        let out = snippet(src, 3, 6, "x.axon");
13257        assert!(out.contains("--> x.axon:3:6"));
13258        assert!(out.contains("1 | line one"));
13259        assert!(out.contains("2 | line two"));
13260        assert!(out.contains("3 | line three"));
13261        assert!(out.contains("4 | line four"));
13262        assert!(out.contains("5 | line five"));
13263        // Caret col 6 → 5-space pad. Empty gutter is 1 space (gutter=1).
13264        assert!(out.contains("\n  |      ^"), "out:\n{out}");
13265    }
13266
13267    #[test]
13268    fn caret_column_one_renders_correctly() {
13269        let out = snippet("abc\n", 1, 1, "<source>");
13270        assert!(out.contains("\n  | ^"));
13271    }
13272
13273    #[test]
13274    fn first_line_clamps_context_before_to_zero() {
13275        let src = "first\nsecond\nthird\nfourth\nfifth";
13276        let out = snippet(src, 1, 1, "<source>");
13277        assert!(out.contains("1 | first"));
13278        assert!(out.contains("2 | second"));
13279        assert!(out.contains("3 | third"));
13280        assert!(!out.contains("4 | fourth"));
13281    }
13282
13283    #[test]
13284    fn last_line_clamps_context_after_to_eof() {
13285        let src = "first\nsecond\nthird\nfourth\nfifth";
13286        let out = snippet(src, 5, 2, "<source>");
13287        assert!(out.contains("5 | fifth"));
13288        assert!(out.contains("3 | third"));
13289        assert!(out.contains("4 | fourth"));
13290        assert!(!out.contains("2 | second"));
13291    }
13292
13293    #[test]
13294    fn gutter_width_grows_with_line_count() {
13295        let src: String = (1..=12).map(|i| format!("line{i}")).collect::<Vec<_>>().join("\n");
13296        let out = snippet(&src, 12, 1, "<source>");
13297        assert!(out.contains("12 | line12"));
13298        assert!(out.contains("10 | line10"));
13299    }
13300
13301    // ── Edge cases ──────────────────────────────────────────────
13302
13303    #[test]
13304    fn empty_source_returns_empty() {
13305        assert_eq!(snippet("", 1, 1, "<source>"), "");
13306    }
13307
13308    #[test]
13309    fn zero_line_returns_empty() {
13310        assert_eq!(snippet("hi", 0, 1, "<source>"), "");
13311    }
13312
13313    #[test]
13314    fn out_of_range_line_returns_empty() {
13315        assert_eq!(snippet("hi", 99, 1, "<source>"), "");
13316    }
13317
13318    #[test]
13319    fn caret_clamps_past_eol() {
13320        let out = snippet("hello", 1, 50, "<source>");
13321        assert!(out.contains("\n  |      ^"), "out:\n{out}");
13322    }
13323
13324    #[test]
13325    fn unicode_codepoint_count_for_caret_clamp() {
13326        // "héllo" = 5 codepoints; column past EOL clamps to 6.
13327        let out = snippet("héllo", 1, 99, "<source>");
13328        assert!(out.contains("\n  |      ^"), "out:\n{out}");
13329    }
13330
13331    #[test]
13332    fn trailing_newline_does_not_create_phantom_last_line() {
13333        let out = snippet("first\nsecond\n", 2, 1, "<source>");
13334        assert!(!out.contains("3 |"));
13335        assert!(out.contains("2 | second"));
13336    }
13337
13338    // ── Parser attach plumbing ──────────────────────────────────
13339
13340    fn lex(src: &str) -> Vec<Token> {
13341        Lexer::new(src, "<test>").tokenize().expect("lex")
13342    }
13343
13344    #[test]
13345    fn strict_parse_attaches_snippet_when_source_given() {
13346        let src = "garbage_token\nflow F() { }";
13347        let err = Parser::new(lex(src))
13348            .with_source(src, "x.axon")
13349            .parse()
13350            .expect_err("must error");
13351        assert!(err.source_snippet.is_some());
13352        let display = format!("{err}");
13353        assert!(display.contains("--> x.axon:"), "display: {display}");
13354    }
13355
13356    #[test]
13357    fn strict_parse_no_snippet_when_no_source() {
13358        let src = "garbage_token";
13359        let err = Parser::new(lex(src)).parse().expect_err("must error");
13360        assert!(err.source_snippet.is_none());
13361        let display = format!("{err}");
13362        assert!(!display.contains("\n  -->"));
13363    }
13364
13365    #[test]
13366    fn every_recovered_error_has_snippet() {
13367        let src = "garbage1\nflow F() { }\ngarbage2\nflow G() { }";
13368        let result = Parser::new(lex(src))
13369            .with_source(src, "multi.axon")
13370            .parse_with_recovery();
13371        assert!(!result.errors.is_empty());
13372        for err in &result.errors {
13373            assert!(err.source_snippet.is_some());
13374            let display = format!("{err}");
13375            assert!(
13376                display.contains("--> multi.axon:"),
13377                "display: {display}"
13378            );
13379        }
13380    }
13381
13382    #[test]
13383    fn recovery_no_snippet_when_no_source() {
13384        let src = "garbage1 garbage2";
13385        let result = Parser::new(lex(src)).parse_with_recovery();
13386        for err in &result.errors {
13387            assert!(err.source_snippet.is_none());
13388        }
13389    }
13390
13391    #[test]
13392    fn snippet_points_at_correct_line_for_each_error() {
13393        let src = "garbage_a\nflow F() { }\ngarbage_b\nflow G() { }";
13394        let result = Parser::new(lex(src))
13395            .with_source(src, "x")
13396            .parse_with_recovery();
13397        for err in &result.errors {
13398            let sn = err.source_snippet.as_ref().expect("snippet");
13399            assert_eq!(sn.line, err.line);
13400        }
13401    }
13402
13403    // ── Backwards-compat ────────────────────────────────────────
13404
13405    #[test]
13406    fn legacy_constructor_still_works() {
13407        let src = "flow F() { }";
13408        let prog = Parser::new(lex(src)).parse().expect("clean");
13409        assert_eq!(prog.declarations.len(), 1);
13410    }
13411
13412    #[test]
13413    fn attach_source_idempotent() {
13414        let err = ParseError {
13415            message: "bad".to_string(),
13416            line: 2,
13417            column: 3,
13418            ..Default::default()
13419        };
13420        let err2 = err.clone().attach_source("a\nb\nc\n", "f.axon");
13421        let first = format!("{err2}");
13422        let err3 = err.attach_source("a\nb\nc\n", "f.axon");
13423        let second = format!("{err3}");
13424        assert_eq!(first, second);
13425    }
13426
13427    #[test]
13428    fn attach_source_noop_when_line_zero() {
13429        let err = ParseError {
13430            message: "bad".to_string(),
13431            line: 0,
13432            column: 0,
13433            ..Default::default()
13434        };
13435        let err = err.attach_source("a\nb\nc\n", "f.axon");
13436        assert!(err.source_snippet.is_none());
13437    }
13438
13439    // ── Cross-stack golden parity ───────────────────────────────
13440    // These golden strings are duplicated verbatim in the Python
13441    // test pack at `tests/test_fase28_source_context.py::TestRustParityShape`.
13442    // Edits here MUST be mirrored in the Python pack — D7.
13443
13444    #[test]
13445    fn golden_simple_three_line_block() {
13446        let src = "alpha\nbeta\ngamma";
13447        let out = snippet(src, 2, 3, "g.axon");
13448        // Note: gutter=1, so empty_gutter=" " (one space). The
13449        // " --> ..." line therefore starts with two spaces ("<empty>"
13450        // + literal " --> ...").
13451        let expected = concat!(
13452            "  --> g.axon:2:3\n",
13453            "  |\n",
13454            "1 | alpha\n",
13455            "2 | beta\n",
13456            "  |   ^\n",
13457            "3 | gamma",
13458        );
13459        assert_eq!(out, expected);
13460    }
13461
13462    #[test]
13463    fn golden_first_line_caret() {
13464        let src = "abc\ndef\n";
13465        let out = snippet(src, 1, 1, "x");
13466        let expected = concat!(
13467            "  --> x:1:1\n",
13468            "  |\n",
13469            "1 | abc\n",
13470            "  | ^\n",
13471            "2 | def",
13472        );
13473        assert_eq!(out, expected);
13474    }
13475
13476    #[test]
13477    fn golden_two_digit_gutter() {
13478        let src: String = (1..=11)
13479            .map(|i| format!("L{i}"))
13480            .collect::<Vec<_>>()
13481            .join("\n");
13482        let out = snippet(&src, 10, 2, "big");
13483        let expected = concat!(
13484            "   --> big:10:2\n",
13485            "   |\n",
13486            " 8 | L8\n",
13487            " 9 | L9\n",
13488            "10 | L10\n",
13489            "   |  ^\n",
13490            "11 | L11",
13491        );
13492        assert_eq!(out, expected);
13493    }
13494}
13495
13496// ── v1.20.0 — Parser integration tests for smart-suggest ──────────────────
13497//
13498// Mirror of `tests/test_fase28_smart_suggest.py::TestParserIntegration`.
13499// Verifies that the parser actually wires `suggest_for` into the
13500// unknown-keyword diagnostic at both error sites — top-level and
13501// flow-body.
13502#[cfg(test)]
13503mod smart_suggest_parser_tests {
13504    use super::*;
13505    use crate::lexer::Lexer;
13506
13507    fn lex(src: &str) -> Vec<Token> {
13508        Lexer::new(src, "<test>").tokenize().expect("lex")
13509    }
13510
13511    #[test]
13512    fn top_level_typo_suggests_flow() {
13513        let src = "flwo F() { }";
13514        let err = Parser::new(lex(src)).parse().expect_err("must error");
13515        assert!(
13516            err.message.contains("Did you mean `flow`?"),
13517            "msg: {}",
13518            err.message
13519        );
13520    }
13521
13522    #[test]
13523    fn top_level_unknown_far_no_suggestion() {
13524        let src = "qwerty F() { }";
13525        let err = Parser::new(lex(src)).parse().expect_err("must error");
13526        assert!(
13527            !err.message.contains("Did you mean"),
13528            "msg: {}",
13529            err.message
13530        );
13531    }
13532
13533    #[test]
13534    fn flow_body_typo_suggests_step() {
13535        let src = "flow F() { stepp S {} }";
13536        let err = Parser::new(lex(src)).parse().expect_err("must error");
13537        assert!(
13538            err.message.contains("Did you mean `step`"),
13539            "msg: {}",
13540            err.message
13541        );
13542    }
13543
13544    #[test]
13545    fn flow_body_typo_suggests_reason() {
13546        let src = "flow F() { reasn R {} }";
13547        let err = Parser::new(lex(src)).parse().expect_err("must error");
13548        assert!(
13549            err.message.contains("Did you mean `reason`?"),
13550            "msg: {}",
13551            err.message
13552        );
13553    }
13554
13555    #[test]
13556    fn recovery_mode_carries_hint() {
13557        let src = "flwo F() { }";
13558        let result = Parser::new(lex(src)).parse_with_recovery();
13559        assert!(
13560            result
13561                .errors
13562                .iter()
13563                .any(|e| e.message.contains("Did you mean `flow`?")),
13564            "errors: {:?}",
13565            result.errors
13566        );
13567    }
13568}
13569
13570// ── v1.30.0 — mutate / purge where-clause capture ────────────────
13571
13572#[cfg(test)]
13573mod mutate_purge_where_tests {
13574    use super::*;
13575
13576    fn parse(src: &str) -> Program {
13577        let tokens = crate::lexer::Lexer::new(src, "<test>")
13578            .tokenize()
13579            .expect("lex");
13580        Parser::new(tokens).parse().expect("parse")
13581    }
13582
13583    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
13584        for d in &prog.declarations {
13585            if let Declaration::Flow(f) = d {
13586                if f.name == flow {
13587                    return f.body.first().expect("flow has at least one step");
13588                }
13589            }
13590        }
13591        panic!("flow `{flow}` not found");
13592    }
13593
13594    #[test]
13595    fn mutate_captures_its_where_clause() {
13596        // Pre-35.m the `{ where: }` block was skipped — every mutate
13597        // ran whole-store. It must now reach `where_expr`.
13598        let prog =
13599            parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
13600        match first_step(&prog, "F") {
13601            FlowStep::Mutate(m) => {
13602                assert_eq!(m.store_name, "accounts");
13603                assert_eq!(m.where_expr, "id = 1");
13604            }
13605            other => panic!("expected Mutate, got {other:?}"),
13606        }
13607    }
13608
13609    #[test]
13610    fn purge_captures_its_where_clause() {
13611        let prog =
13612            parse("flow F() -> Unit { purge logs { where: \"ts < 100\" } }");
13613        match first_step(&prog, "F") {
13614            FlowStep::Purge(p) => {
13615                assert_eq!(p.store_name, "logs");
13616                assert_eq!(p.where_expr, "ts < 100");
13617            }
13618            other => panic!("expected Purge, got {other:?}"),
13619        }
13620    }
13621
13622    #[test]
13623    fn mutate_without_a_where_block_is_a_whole_store_op() {
13624        // No `{ where: }` → an empty filter → the runtime renders
13625        // `WHERE TRUE` (every row). A valid, intentional op.
13626        let prog = parse("flow F() -> Unit { mutate accounts }");
13627        match first_step(&prog, "F") {
13628            FlowStep::Mutate(m) => {
13629                assert_eq!(m.store_name, "accounts");
13630                assert_eq!(m.where_expr, "");
13631            }
13632            other => panic!("expected Mutate, got {other:?}"),
13633        }
13634    }
13635}
13636
13637// ── v1.30.0 — persist field-block capture ────────────────────────
13638
13639#[cfg(test)]
13640mod persist_fields_tests {
13641    use super::*;
13642
13643    fn parse(src: &str) -> Program {
13644        let tokens = crate::lexer::Lexer::new(src, "<test>")
13645            .tokenize()
13646            .expect("lex");
13647        Parser::new(tokens).parse().expect("parse")
13648    }
13649
13650    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
13651        for d in &prog.declarations {
13652            if let Declaration::Flow(f) = d {
13653                if f.name == flow {
13654                    return f.body.first().expect("flow has at least one step");
13655                }
13656            }
13657        }
13658        panic!("flow `{flow}` not found");
13659    }
13660
13661    #[test]
13662    fn persist_captures_its_field_block() {
13663        // Pre-35.o the `{ col: value }` block was skipped — every
13664        // persist wrote the whole binding context. It must now reach
13665        // `fields`, in source order, with value expressions raw.
13666        let prog = parse(
13667            "flow F() -> Unit { persist into chat_history { \
13668             session_id: \"${session_id}\" sender: \"user\" \
13669             content: \"${message}\" } }",
13670        );
13671        match first_step(&prog, "F") {
13672            FlowStep::Persist(p) => {
13673                assert_eq!(p.store_name, "chat_history");
13674                assert_eq!(
13675                    p.fields,
13676                    vec![
13677                        ("session_id".to_string(), "${session_id}".to_string()),
13678                        ("sender".to_string(), "user".to_string()),
13679                        ("content".to_string(), "${message}".to_string()),
13680                    ]
13681                );
13682            }
13683            other => panic!("expected Persist, got {other:?}"),
13684        }
13685    }
13686
13687    #[test]
13688    fn persist_without_a_block_keeps_the_user_bindings_fallback() {
13689        // No `{ }` → empty `fields` → the runtime falls back to the
13690        // v1.30.0 user-bindings row. Backward-compatible.
13691        let prog = parse("flow F() -> Unit { persist events }");
13692        match first_step(&prog, "F") {
13693            FlowStep::Persist(p) => {
13694                assert_eq!(p.store_name, "events");
13695                assert!(p.fields.is_empty());
13696            }
13697            other => panic!("expected Persist, got {other:?}"),
13698        }
13699    }
13700
13701    #[test]
13702    fn persist_accepts_the_optional_into_connector() {
13703        // `persist into X` and `persist X` resolve to the SAME store
13704        // name — pre-35.o `into` was captured AS the store name.
13705        let with =
13706            parse("flow F() -> Unit { persist into accounts { id: \"1\" } }");
13707        let without =
13708            parse("flow F() -> Unit { persist accounts { id: \"1\" } }");
13709        for prog in [&with, &without] {
13710            match first_step(prog, "F") {
13711                FlowStep::Persist(p) => assert_eq!(p.store_name, "accounts"),
13712                other => panic!("expected Persist, got {other:?}"),
13713            }
13714        }
13715    }
13716
13717    #[test]
13718    fn persist_into_without_a_block_resolves_the_store_name() {
13719        // `persist into events` — the `into` connector is skipped, the
13720        // store name is `events` (not `into`). Lateral bug closed.
13721        let prog = parse("flow F() -> Unit { persist into events }");
13722        match first_step(&prog, "F") {
13723            FlowStep::Persist(p) => {
13724                assert_eq!(p.store_name, "events");
13725                assert!(p.fields.is_empty());
13726            }
13727            other => panic!("expected Persist, got {other:?}"),
13728        }
13729    }
13730
13731    #[test]
13732    fn persist_fields_lower_into_the_ir() {
13733        // The IR generator must carry `fields` onto `IRPersistStep`
13734        // so the runtime reads exactly the declared columns.
13735        let prog = parse(
13736            "flow F() -> Unit { persist into chat { content: \"${msg}\" } }",
13737        );
13738        let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
13739        let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
13740        match flow.steps.first().expect("one step") {
13741            crate::ir_nodes::IRFlowNode::Persist(p) => {
13742                assert_eq!(p.store_name, "chat");
13743                assert_eq!(
13744                    p.fields,
13745                    vec![("content".to_string(), "${msg}".to_string())]
13746                );
13747            }
13748            other => panic!("expected IRFlowNode::Persist, got {other:?}"),
13749        }
13750    }
13751}
13752
13753// ── v1.30.0 — mutate SET-field-block capture ─────────────────────
13754
13755#[cfg(test)]
13756mod mutate_fields_tests {
13757    use super::*;
13758
13759    fn parse(src: &str) -> Program {
13760        let tokens = crate::lexer::Lexer::new(src, "<test>")
13761            .tokenize()
13762            .expect("lex");
13763        Parser::new(tokens).parse().expect("parse")
13764    }
13765
13766    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
13767        for d in &prog.declarations {
13768            if let Declaration::Flow(f) = d {
13769                if f.name == flow {
13770                    return f.body.first().expect("flow has at least one step");
13771                }
13772            }
13773        }
13774        panic!("flow `{flow}` not found");
13775    }
13776
13777    #[test]
13778    fn mutate_captures_its_set_field_block() {
13779        // Pre-35.p every key but `where:` was skipped — the runtime
13780        // SET every flow binding. The SET columns must now reach
13781        // `fields`, in source order, with `where:` still captured.
13782        let prog = parse(
13783            "flow F() -> Unit { mutate accounts { where: \"id = ${id}\" \
13784             balance: \"${new_balance}\" status: \"active\" } }",
13785        );
13786        match first_step(&prog, "F") {
13787            FlowStep::Mutate(m) => {
13788                assert_eq!(m.store_name, "accounts");
13789                assert_eq!(m.where_expr, "id = ${id}");
13790                assert_eq!(
13791                    m.fields,
13792                    vec![
13793                        ("balance".to_string(), "${new_balance}".to_string()),
13794                        ("status".to_string(), "active".to_string()),
13795                    ]
13796                );
13797            }
13798            other => panic!("expected Mutate, got {other:?}"),
13799        }
13800    }
13801
13802    #[test]
13803    fn mutate_where_only_block_has_no_set_fields() {
13804        // A `{ where: }`-only block → empty `fields` → the runtime
13805        // falls back to the v1.31.0 user-bindings SET.
13806        let prog =
13807            parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
13808        match first_step(&prog, "F") {
13809            FlowStep::Mutate(m) => {
13810                assert_eq!(m.where_expr, "id = 1");
13811                assert!(m.fields.is_empty());
13812            }
13813            other => panic!("expected Mutate, got {other:?}"),
13814        }
13815    }
13816
13817    #[test]
13818    fn mutate_with_no_block_is_a_whole_store_op() {
13819        // No block at all → empty where + empty fields (a whole-store
13820        // UPDATE from user bindings) — unchanged from 35.m.
13821        let prog = parse("flow F() -> Unit { mutate accounts }");
13822        match first_step(&prog, "F") {
13823            FlowStep::Mutate(m) => {
13824                assert_eq!(m.store_name, "accounts");
13825                assert_eq!(m.where_expr, "");
13826                assert!(m.fields.is_empty());
13827            }
13828            other => panic!("expected Mutate, got {other:?}"),
13829        }
13830    }
13831
13832    #[test]
13833    fn mutate_fields_lower_into_the_ir() {
13834        let prog = parse(
13835            "flow F() -> Unit { mutate t { where: \"id = 1\" v: \"${x}\" } }",
13836        );
13837        let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
13838        let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
13839        match flow.steps.first().expect("one step") {
13840            crate::ir_nodes::IRFlowNode::Mutate(m) => {
13841                assert_eq!(m.where_expr, "id = 1");
13842                assert_eq!(
13843                    m.fields,
13844                    vec![("v".to_string(), "${x}".to_string())]
13845                );
13846            }
13847            other => panic!("expected IRFlowNode::Mutate, got {other:?}"),
13848        }
13849    }
13850}
13851