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            name,
2586            provider: String::new(),
2587            max_results: None,
2588            filter_expr: String::new(),
2589            timeout: String::new(),
2590            runtime: String::new(),
2591            resource_ref: String::new(),
2592            sandbox: None,
2593            effects: None,
2594            parameters: Vec::new(),
2595            output_type: None,
2596            requires: Vec::new(),
2597            secret: String::new(),
2598            secret_partition: String::new(),
2599            target: None,
2600            risk: None,
2601            argv: Vec::new(),
2602            cache: String::new(),
2603            scrape: None,
2604            loc,
2605            leading_trivia: Vec::new(),
2606            trailing_trivia: Vec::new(),
2607        };
2608
2609        // v2.39.0/the design decision — unknown fields are recorded (not silently
2610        // skipped) so a `target:`-bound technician tool can HARD-ERROR on one
2611        // (a typo'd safety field must never quietly disable a guard), while a
2612        // legacy schema-less tool keeps its lenient record-and-skip (zero
2613        // regression). The decision is deferred to after the block is parsed,
2614        // since `target:` may appear after the unknown field.
2615        let mut unknown_fields: Vec<(String, u32, u32)> = Vec::new();
2616
2617        while !self.check(TokenType::RBrace) {
2618            let field_tok = self.current().clone();
2619            let field_name = field_tok.value.clone();
2620            self.advance();
2621            self.consume(TokenType::Colon)?;
2622
2623            match field_name.as_str() {
2624                "provider" => node.provider = self.consume_any_ident_or_kw()?.value,
2625                "max_results" => {
2626                    node.max_results = Some(
2627                        self.consume(TokenType::Integer)?
2628                            .value
2629                            .parse::<i64>()
2630                            .unwrap_or(0),
2631                    )
2632                }
2633                "filter" => node.filter_expr = self.parse_filter_expression()?,
2634                "timeout" => node.timeout = self.consume(TokenType::Duration)?.value,
2635                "runtime" => node.runtime = self.consume_any_ident_or_kw()?.value,
2636                // v2.69.0 — the `resource` this tool's channel runs on. The
2637                // channel's address, concurrency and lifecycle come from it;
2638                // `runtime:` then names the path within the channel.
2639                "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
2640                "sandbox" => node.sandbox = Some(self.parse_bool()?),
2641                "effects" => node.effects = Some(self.parse_effect_row()?),
2642                // v2.8.0 — the tool's typed input schema + output type.
2643                "parameters" => node.parameters = self.parse_tool_param_schema()?,
2644                "output_type" => node.output_type = Some(self.parse_output_type_string()?),
2645                // v2.77.0 — the tool's required authorization
2646                // scopes: bare dot-separated capability slugs, the EXACT
2647                // grammar + charset of `credential.grants` (v2.46.0) so the two
2648                // vocabularies are one. `requires: [w_organization_social,
2649                // video.publish]`. Subset coverage is `axon-T956`.
2650                "requires" => {
2651                    let bracket_tok = self.current().clone();
2652                    let items = self.parse_bracketed_dot_identifiers()?;
2653                    for slug in &items {
2654                        if !is_valid_capability_slug(slug) {
2655                            return Err(ParseError {
2656                                message: format!(
2657                                    "Invalid capability slug '{slug}' in tool '{}' \
2658                                     `requires:`. Scope slugs must match \
2659                                     ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — the same \
2660                                     grammar as `credential.grants`. Examples: \
2661                                     `w_organization_social`, `video.publish`.",
2662                                    node.name
2663                                ),
2664                                line: bracket_tok.line,
2665                                column: bracket_tok.column,
2666                                ..Default::default()
2667                            });
2668                        }
2669                    }
2670                    node.requires = items;
2671                }
2672                // v2.48.0 — the per-tenant secret KEY injected at
2673                // dispatch (`rotation_without_revelation`). Key shape +
2674                // technician exclusion are `axon-T902` (type-checker).
2675                "secret" => node.secret = self.parse_dotted_identifier()?,
2676                // v2.49.0 — `secret_partition:` names one of this tool's
2677                // own `parameters:` (a bare identifier, NOT dotted — it is a
2678                // parameter reference, not a key). Its runtime value becomes
2679                // a single appended key segment at dispatch. The membership +
2680                // `String`-type + technician laws are `axon-T903`.
2681                "secret_partition" => {
2682                    node.secret_partition = self.consume_any_ident_or_kw()?.value
2683                }
2684                // v2.39.0 — Remote Hands technician fields.
2685                "target" => node.target = Some(self.consume_any_ident_or_kw()?.value),
2686                "risk" => node.risk = Some(self.consume_any_ident_or_kw()?.value),
2687                // The argv template: a bracketed list of quoted elements
2688                // (`argv: ["ping", "-c", "${count}", "${host}"]`). Reuses the
2689                // CORS list helper (tolerant of `[]` and a trailing comma).
2690                "argv" => node.argv = self.parse_bracketed_strings()?,
2691                // v2.40.0 — the tool's result-memoization policy reference
2692                // (a declared `cache` name, or the `none` opt-out sentinel).
2693                "cache" => node.cache = self.consume_any_ident_or_kw()?.value,
2694                // v2.52.0 — the closed-catalog web-acquisition config
2695                // block. `scrape: { engine: …, extract: […], … }`.
2696                "scrape" => node.scrape = Some(self.parse_scrape_spec()?),
2697                _ => {
2698                    unknown_fields.push((field_name, field_tok.line, field_tok.column));
2699                    self.skip_value();
2700                }
2701            }
2702        }
2703        self.consume(TokenType::RBrace)?;
2704
2705        // v2.39.0/the design decision — a `target:`-bound tool opts into strict field
2706        // checking. An unknown field on it is a parse error, mirroring the v2.38.0
2707        // `cors`/`voice` closed-catalog discipline — but scoped to the
2708        // technician surface so ordinary tools are untouched.
2709        // v2.52.0 — a `scrape:`-bearing web-acquisition tool opts
2710        // into the same strictness: a typo'd safety field (e.g. a mis-spelled
2711        // `respect_robots`) must never quietly disable a guard.
2712        if node.target.is_some() || node.scrape.is_some() {
2713            if let Some((field_name, line, column)) = unknown_fields.into_iter().next() {
2714                let (surface, valid) = if node.target.is_some() {
2715                    (
2716                        "technician tool",
2717                        "provider, parameters, output_type, timeout, effects, target, risk, argv",
2718                    )
2719                } else {
2720                    (
2721                        "web-acquisition tool",
2722                        "provider, parameters, output_type, timeout, effects, secret, \
2723                         secret_partition, cache, scrape",
2724                    )
2725                };
2726                return Err(ParseError {
2727                    message: format!(
2728                        "unknown field `{field_name}` in {surface} `{}` — this tool uses \
2729                         strict field checking; valid fields: {valid}",
2730                        node.name
2731                    ),
2732                    line,
2733                    column,
2734                    ..Default::default()
2735                });
2736            }
2737        }
2738        Ok(node)
2739    }
2740
2741    /// v2.52.0 — parse the closed-catalog `scrape: { … }` web-acquisition
2742    /// config sub-block. Every field is optional; an unknown field is a hard
2743    /// parse error (the v2.38.0 `cors` closed-catalog discipline). Mirrors the
2744    /// field grammar of `parse_tool` for the scrape-specific keys.
2745    fn parse_scrape_spec(&mut self) -> Result<crate::ast::ScrapeSpec, ParseError> {
2746        let open = self.consume(TokenType::LBrace)?;
2747        let loc = self.loc_of(&open);
2748        let mut spec = crate::ast::ScrapeSpec {
2749            loc,
2750            ..Default::default()
2751        };
2752        while !self.check(TokenType::RBrace) {
2753            let field_tok = self.current().clone();
2754            let field_name = field_tok.value.clone();
2755            self.advance();
2756            self.consume(TokenType::Colon)?;
2757            match field_name.as_str() {
2758                "engine" => spec.engine = Some(self.consume_any_ident_or_kw()?.value),
2759                "impersonate" => spec.impersonate = Some(self.consume_any_ident_or_kw()?.value),
2760                "render_wait" => spec.render_wait = Some(self.consume(TokenType::Duration)?.value),
2761                "proxy" => spec.proxy = self.parse_dotted_identifier()?,
2762                "respect_robots" => spec.respect_robots = Some(self.parse_bool()?),
2763                "extract" => spec.extract = self.parse_bracketed_strings()?,
2764                "adaptive" => spec.adaptive = Some(self.parse_bool()?),
2765                "similarity_floor" => spec.similarity_floor = self.parse_optional_float(),
2766                "follow" => spec.follow = self.consume(TokenType::StringLit)?.value,
2767                "max_depth" => {
2768                    spec.max_depth =
2769                        Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2770                }
2771                "max_pages" => {
2772                    spec.max_pages =
2773                        Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2774                }
2775                "concurrency" => {
2776                    spec.concurrency =
2777                        Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2778                }
2779                "politeness" => spec.politeness = self.consume_any_ident_or_kw()?.value,
2780                "checkpoint" => spec.checkpoint = self.consume_any_ident_or_kw()?.value,
2781                other => {
2782                    return Err(self.error(&format!(
2783                        "unknown scrape field `{other}` — the `scrape: {{ … }}` block is a \
2784                         closed catalog; valid fields: engine, impersonate, \
2785                         render_wait, proxy, respect_robots, extract, adaptive, \
2786                         similarity_floor, follow, max_depth, max_pages, concurrency, \
2787                         politeness, checkpoint"
2788                    )));
2789                }
2790            }
2791        }
2792        self.consume(TokenType::RBrace)?;
2793        Ok(spec)
2794    }
2795
2796    /// v2.8.0 — parse a tool's INPUT SCHEMA: a brace-delimited list of
2797    /// `name: Type` parameters (`parameters: { query: String, max_results: Int }`).
2798    /// Reuses the flow-parameter shape (`Parameter`), so the same `TypeExpr`
2799    /// grammar — generics like `List<T>`, `?`-optionals — applies. A trailing
2800    /// comma is tolerated; an empty `{}` yields no parameters.
2801    fn parse_tool_param_schema(&mut self) -> Result<Vec<Parameter>, ParseError> {
2802        self.consume(TokenType::LBrace)?;
2803        let mut params = Vec::new();
2804        while !self.check(TokenType::RBrace) {
2805            // Accept a keyword-as-name (`filter`, `type`, `domain`, …) — real
2806            // adopter tool schemas use such parameter names; the `:` after it
2807            // disambiguates.
2808            let name = self.consume_any_ident_or_kw()?;
2809            let ploc = self.loc_of(&name);
2810            self.consume(TokenType::Colon)?;
2811            let type_expr = self.parse_type_expr()?;
2812            params.push(Parameter {
2813                name: name.value,
2814                type_expr,
2815                loc: ploc,
2816            });
2817            if self.check(TokenType::Comma) {
2818                self.advance();
2819            } else {
2820                break;
2821            }
2822        }
2823        self.consume(TokenType::RBrace)?;
2824        Ok(params)
2825    }
2826
2827    fn parse_filter_expression(&mut self) -> Result<String, ParseError> {
2828        let name = self.consume_any_ident_or_kw()?.value;
2829        if self.check(TokenType::LParen) {
2830            self.advance();
2831            let mut parts = vec![name, "(".to_string()];
2832            while !self.check(TokenType::RParen) {
2833                parts.push(self.advance().value.clone());
2834            }
2835            self.consume(TokenType::RParen)?;
2836            parts.push(")".to_string());
2837            Ok(parts.join(""))
2838        } else {
2839            Ok(name)
2840        }
2841    }
2842
2843    fn parse_effect_row(&mut self) -> Result<EffectRow, ParseError> {
2844        let tok = self.consume(TokenType::Lt)?;
2845        let loc = self.loc_of(&tok);
2846        let mut effects = Vec::new();
2847        let mut epistemic_level = String::new();
2848
2849        while !self.check(TokenType::Gt) {
2850            let name = self.consume_any_ident_or_kw()?.value;
2851            if self.check(TokenType::Colon) {
2852                self.advance();
2853                // v1.4.0 — qualifiers can be compound slugs
2854                // from a closed catalogue:
2855                //
2856                //   * dot-separated  — `legal:HIPAA.164_502`,
2857                //                       `legal:GDPR.Art6.Consent`,
2858                //                       `legal:PCI_DSS.v4_Req3`
2859                //   * colon-separated — `ots:transform:mulaw8:pcm16`,
2860                //                       `ots:backend:native`
2861                //   * mixed           — supported by the same loop.
2862                //
2863                // The lexer fragments dotted slugs across IDENT /
2864                // INTEGER tokens (e.g., `164_502` lexes as INTEGER
2865                // `164` + IDENT `_502` because `_` starts a fresh
2866                // identifier); we recombine here using source-column
2867                // adjacency so the type checker sees the catalog
2868                // string verbatim.
2869                let level = self.parse_qualifier_value()?;
2870                if name == "epistemic" {
2871                    epistemic_level = level;
2872                } else {
2873                    effects.push(format!("{name}:{level}"));
2874                }
2875            } else {
2876                effects.push(name);
2877            }
2878            if self.check(TokenType::Comma) {
2879                self.advance();
2880            }
2881        }
2882        self.consume(TokenType::Gt)?;
2883
2884        Ok(EffectRow {
2885            effects,
2886            epistemic_level,
2887            loc,
2888        })
2889    }
2890
2891    /// Parse a compound qualifier value following an effect's first
2892    /// colon — supports both dot-separated (`HIPAA.164_502`) and
2893    /// colon-separated (`transform:mulaw8:pcm16`) catalogue slugs, as
2894    /// well as mixed forms.
2895    ///
2896    /// The grammar is: `segment ((`.` | `:`) segment)*` where a
2897    /// segment is a contiguous run of IDENT / INTEGER tokens (see
2898    /// [`Self::consume_dotted_slug_segment`]).
2899    fn parse_qualifier_value(&mut self) -> Result<String, ParseError> {
2900        let mut buf = self.consume_dotted_slug_segment()?;
2901        loop {
2902            let sep = if self.check(TokenType::Dot) {
2903                '.'
2904            } else if self.check(TokenType::Colon) {
2905                ':'
2906            } else {
2907                break;
2908            };
2909            self.advance();
2910            let part = self.consume_dotted_slug_segment()?;
2911            buf.push(sep);
2912            buf.push_str(&part);
2913        }
2914        Ok(buf)
2915    }
2916
2917    /// Consume a contiguous run of IDENT / INTEGER / keyword-ident
2918    /// tokens whose source positions are adjacent (no whitespace
2919    /// between them), concatenating their text into a single segment.
2920    ///
2921    /// Needed for closed-catalogue qualifier slugs whose segment
2922    /// mixes digits and identifier characters — e.g. `HIPAA.164_502`
2923    /// lexes as INTEGER `164` + IDENT `_502` because `_` starts a
2924    /// fresh identifier; the catalog value is the concatenation
2925    /// `164_502`. Adjacency is determined by matching
2926    /// `(line, column + len)` of the previous token against the next
2927    /// token's start position.
2928    fn consume_dotted_slug_segment(&mut self) -> Result<String, ParseError> {
2929        let first = self.consume_any_ident_or_kw()?;
2930        let mut buf = first.value.clone();
2931        let mut next_line = first.line;
2932        let mut next_col = first.column + first.value.chars().count() as u32;
2933        loop {
2934            let cur = self.current();
2935            let is_segment_token = matches!(cur.ttype, TokenType::Identifier | TokenType::Integer,);
2936            if !is_segment_token {
2937                break;
2938            }
2939            if cur.line != next_line || cur.column != next_col {
2940                break;
2941            }
2942            buf.push_str(&cur.value);
2943            next_col = cur.column + cur.value.chars().count() as u32;
2944            next_line = cur.line;
2945            self.pos += 1;
2946        }
2947        Ok(buf)
2948    }
2949
2950    // ── TYPE ─────────────────────────────────────────────────────
2951
2952    fn parse_type_def(&mut self) -> Result<TypeDefinition, ParseError> {
2953        let tok = self.consume(TokenType::Type)?;
2954        let loc = self.loc_of(&tok);
2955        let name = self.consume(TokenType::Identifier)?.value;
2956
2957        let mut node = TypeDefinition {
2958            name,
2959            fields: Vec::new(),
2960            range_constraint: None,
2961            where_clause: None,
2962            compliance: Vec::new(),
2963            loc: loc.clone(),
2964            leading_trivia: Vec::new(),
2965            trailing_trivia: Vec::new(),
2966        };
2967
2968        // Optional range: (0.0..1.0)
2969        if self.check(TokenType::LParen) {
2970            self.advance();
2971            let min_val = self.consume_number()?;
2972            self.consume(TokenType::DotDot)?;
2973            let max_val = self.consume_number()?;
2974            self.consume(TokenType::RParen)?;
2975            node.range_constraint = Some(RangeConstraint {
2976                min_value: min_val,
2977                max_value: max_val,
2978                loc: loc.clone(),
2979            });
2980        }
2981
2982        // Optional where clause
2983        if self.check(TokenType::Where) {
2984            self.advance();
2985            let mut expr_parts = Vec::new();
2986            while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
2987                if self.check(TokenType::Eof) {
2988                    break;
2989                }
2990                expr_parts.push(self.advance().value.clone());
2991            }
2992            node.where_clause = Some(WhereClause {
2993                expression: expr_parts.join(" "),
2994                loc: loc.clone(),
2995            });
2996        }
2997
2998        // Optional ESK — `compliance [HIPAA, ...]` prefix modifier
2999        // between `type Name` / `range` / `where` and the body `{`.
3000        if self.check(TokenType::Identifier) && self.current().value == "compliance" {
3001            self.advance();
3002            node.compliance = self.parse_bracketed_identifiers()?;
3003        }
3004
3005        // Optional body: { field: Type, ... }
3006        if self.check(TokenType::LBrace) {
3007            self.advance();
3008            while !self.check(TokenType::RBrace) {
3009                let field_name = self.consume(TokenType::Identifier)?;
3010                let field_loc = self.loc_of(&field_name);
3011                self.consume(TokenType::Colon)?;
3012                let type_expr = self.parse_type_expr()?;
3013                node.fields.push(TypeField {
3014                    name: field_name.value,
3015                    type_expr,
3016                    loc: field_loc,
3017                });
3018                if self.check(TokenType::Comma) {
3019                    self.advance();
3020                }
3021            }
3022            self.consume(TokenType::RBrace)?;
3023        }
3024
3025        Ok(node)
3026    }
3027
3028    fn parse_type_expr(&mut self) -> Result<TypeExpr, ParseError> {
3029        // v2.83.0 — a LEADING bracket is the list-type sugar the README
3030        // has always written in flow signatures: `readings: [SensorReading]`
3031        // (blocks 44-45). It lowers to exactly what `List<SensorReading>`
3032        // produces, so nothing downstream learns a new shape — the v2.0.0
3033        // comment below already names `List<T>` as the canonical carrier.
3034        if self.check(TokenType::LBracket) {
3035            let open = self.current().clone();
3036            self.advance();
3037            let inner = self.parse_type_expr()?;
3038            self.consume(TokenType::RBracket)?;
3039            let mut optional = false;
3040            if self.check(TokenType::Question) {
3041                self.advance();
3042                optional = true;
3043            }
3044            return Ok(TypeExpr {
3045                name: "List".to_string(),
3046                generic_param: if inner.generic_param.is_empty() {
3047                    inner.name
3048                } else {
3049                    format!("{}<{}>", inner.name, inner.generic_param)
3050                },
3051                optional,
3052                loc: self.loc_of(&open),
3053            });
3054        }
3055        let name_tok = self.consume(TokenType::Identifier)?;
3056        let loc = self.loc_of(&name_tok);
3057        let mut generic_param = String::new();
3058        let mut optional = false;
3059
3060        if self.check(TokenType::Lt) {
3061            self.advance();
3062            // v2.0.0 — recursive: the generic param can itself be a
3063            // nested type expression. `FlowEnvelope<List<TenantRecord>>`
3064            // parses as outer=FlowEnvelope, inner=List<TenantRecord>.
3065            // Pre-39.a the inner had to be a single Identifier; nested
3066            // generics like the canonical FlowEnvelope<T> wrapper
3067            // required this lift. Backwards-compat preserved for
3068            // single-level generics like `Stream<Token>` and
3069            // `List<T>` — the recursion lands once and returns the
3070            // same flat string the v1.x parser produced.
3071            let inner = self.parse_type_expr()?;
3072            generic_param = if inner.generic_param.is_empty() {
3073                inner.name
3074            } else {
3075                format!("{}<{}>", inner.name, inner.generic_param)
3076            };
3077            self.consume(TokenType::Gt)?;
3078        }
3079        // v2.4.0 — bracket type parameters for the continuous-carrier
3080        // grammar: `SymbolicPtr[Tensor[Float32]]`, `DensityMatrix[1024]`. The
3081        // param is either a nested type expression OR a numeric dimension.
3082        if self.check(TokenType::LBracket) {
3083            self.advance();
3084            if matches!(self.current().ttype, TokenType::Integer | TokenType::Float) {
3085                generic_param = self.advance().value.clone();
3086            } else {
3087                let inner = self.parse_type_expr()?;
3088                generic_param = if inner.generic_param.is_empty() {
3089                    inner.name
3090                } else {
3091                    format!("{}[{}]", inner.name, inner.generic_param)
3092                };
3093            }
3094            self.consume(TokenType::RBracket)?;
3095        }
3096        if self.check(TokenType::Question) {
3097            self.advance();
3098            optional = true;
3099        }
3100
3101        Ok(TypeExpr {
3102            name: name_tok.value,
3103            generic_param,
3104            optional,
3105            loc,
3106        })
3107    }
3108
3109    /// Parse a type expression in a context where the AST stores the
3110    /// shape as a flat string (step / reason / forge / ots-apply
3111    /// productions). Mirrors Python `_parse_output_type_string`.
3112    ///
3113    /// Accepts:
3114    /// - `Identifier`        → `"Identifier"`
3115    /// - `Stream<String>`    → `"Stream<String>"`
3116    /// - `Optional?`         → `"Optional?"`
3117    /// - `Stream<String>?`   → `"Stream<String>?"`
3118    ///
3119    /// **Why this exists** — pre-fix, the step parser called
3120    /// `consume(TokenType::Identifier)?.value` which captured only
3121    /// the head identifier and left `< … >` unconsumed. For
3122    /// `output: Stream<Token>`, this produced `output_type =
3123    /// "Stream"`, and downstream `flow_has_stream_output`'s
3124    /// `starts_with("Stream<") && ends_with('>')` predicate then
3125    /// returned false → `implicit_transport == "json"` → the
3126    /// dynamic-route fallback in `axon-rs` served JSON instead of
3127    /// SSE even when the adopter's source canonically declared the
3128    /// algebraic stream effect. Surfaced 2026-05-12 by adopter
3129    /// `docs/MIGRATION_TO_AXON.md` audit after the v1.23.0 wire-
3130    /// layer didn't honor the declarative effect. Python parser was
3131    /// fixed for the same gap 2026-05-09; this is the Rust cross-
3132    /// stack catch-up.
3133    fn parse_output_type_string(&mut self) -> Result<String, ParseError> {
3134        let expr = self.parse_type_expr()?;
3135        let mut s = expr.name;
3136        if !expr.generic_param.is_empty() {
3137            s.push('<');
3138            s.push_str(&expr.generic_param);
3139            s.push('>');
3140        }
3141        if expr.optional {
3142            s.push('?');
3143        }
3144        Ok(s)
3145    }
3146
3147    // ── FLOW ─────────────────────────────────────────────────────
3148
3149    fn parse_flow(&mut self) -> Result<FlowDefinition, ParseError> {
3150        let tok = self.consume(TokenType::Flow)?;
3151        let loc = self.loc_of(&tok);
3152        let name = self.consume(TokenType::Identifier)?.value;
3153
3154        self.consume(TokenType::LParen)?;
3155        let mut parameters = Vec::new();
3156        if !self.check(TokenType::RParen) {
3157            parameters = self.parse_param_list()?;
3158        }
3159        self.consume(TokenType::RParen)?;
3160
3161        let mut return_type = None;
3162        if self.check(TokenType::Arrow) {
3163            self.advance();
3164            return_type = Some(self.parse_type_expr()?);
3165        }
3166
3167        self.consume(TokenType::LBrace)?;
3168        let mut body = Vec::new();
3169        while !self.check(TokenType::RBrace) {
3170            body.push(self.parse_flow_step()?);
3171        }
3172        self.consume(TokenType::RBrace)?;
3173
3174        Ok(FlowDefinition {
3175            name,
3176            parameters,
3177            return_type,
3178            body,
3179            loc,
3180            leading_trivia: Vec::new(),
3181            trailing_trivia: Vec::new(),
3182        })
3183    }
3184
3185    // ── v2.87.0 — algebraic effects (Plotkin/Pretnar) ─────────────
3186    //
3187    // Four constructs, in the shape `the design plan` section 3.1 publishes verbatim.
3188
3189    /// `effect SSE { Emit(token: Token) -> Unit  Done() -> Never }`
3190    ///
3191    /// The declaration exists so the operation catalog is CLOSED. the design decision's bare
3192    /// `perform Emit(x)` resolves against exactly this set, and an operation
3193    /// two effects both declare is a compile error naming both — not a silent
3194    /// pick. Without the declaration there would be nothing to resolve against
3195    /// and `effect_name` would be a free string, which is the defect
3196    /// `feedback_free_string_field_breeds_fake_catalog` names.
3197    fn parse_effect(&mut self) -> Result<EffectDefinition, ParseError> {
3198        let tok = self.consume(TokenType::Effect)?;
3199        let loc = self.loc_of(&tok);
3200        let name = self.consume_any_ident_or_kw()?.value;
3201        self.consume(TokenType::LBrace)?;
3202
3203        let mut operations: Vec<EffectOperation> = Vec::new();
3204        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3205            let op_tok = self.current().clone();
3206            let op_name = self.consume_any_ident_or_kw()?.value;
3207
3208            self.consume(TokenType::LParen)?;
3209            let parameters = if self.check(TokenType::RParen) {
3210                Vec::new()
3211            } else {
3212                self.parse_param_list()?
3213            };
3214            self.consume(TokenType::RParen)?;
3215
3216            // `-> T` is optional in the grammar; section 3.1 always writes it, and a
3217            // missing return type reads as Unit at the type-checker.
3218            let mut return_type = String::new();
3219            if self.check(TokenType::Arrow) {
3220                self.advance();
3221                return_type = self.parse_type_expr()?.name;
3222            }
3223
3224            // A duplicate operation name inside ONE effect is refused: the
3225            // handler-clause lookup is by operation name, so two declarations
3226            // would make the arity check depend on which one the search found
3227            // first — a defect nobody would ever see fire.
3228            if let Some(prior) = operations.iter().find(|o| o.name == op_name) {
3229                return Err(ParseError {
3230                    message: format!(
3231                        "effect `{name}` declares operation `{op_name}` twice (first at \
3232                         line {}); handler dispatch is by operation NAME, so a second \
3233                         declaration would silently shadow the first",
3234                        prior.loc.line
3235                    ),
3236                    line: op_tok.line,
3237                    column: op_tok.column,
3238                    ..Default::default()
3239                });
3240            }
3241
3242            operations.push(EffectOperation {
3243                name: op_name,
3244                parameters,
3245                return_type,
3246                loc: self.loc_of(&op_tok),
3247            });
3248        }
3249        self.consume(TokenType::RBrace)?;
3250
3251        Ok(EffectDefinition {
3252            name,
3253            operations,
3254            loc,
3255            leading_trivia: Vec::new(),
3256            trailing_trivia: Vec::new(),
3257        })
3258    }
3259
3260    /// `handle SSE { Emit(token) -> { … } } in { … }` — the delimited handler
3261    /// scope (D3).
3262    ///
3263    /// The `in { … }` body is parsed with [`Self::parse_flow_step`], and that is
3264    /// the whole point of the design decision: the body is ORDINARY flow steps, so
3265    /// `run generate(…)` inside a handler runs for real. Lowering it onto
3266    /// `axon-rs`'s `Instruction` alphabet instead would have made every
3267    /// non-effect node in it a `Passthrough` — inert — which is the v2.67.0 defect
3268    /// this cycle exists not to repeat.
3269    fn parse_handle_block(&mut self) -> Result<HandleBlock, ParseError> {
3270        let tok = self.consume(TokenType::Handle)?;
3271        let loc = self.loc_of(&tok);
3272
3273        // `handle E1, E2 { … }` — one frame may intercept several effects.
3274        let mut effect_names = vec![self.consume_any_ident_or_kw()?.value];
3275        while self.check(TokenType::Comma) {
3276            self.advance();
3277            effect_names.push(self.consume_any_ident_or_kw()?.value);
3278        }
3279
3280        self.consume(TokenType::LBrace)?;
3281        let mut clauses: Vec<HandlerClause> = Vec::new();
3282        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3283            let clause_tok = self.current().clone();
3284            let operation_name = self.consume_any_ident_or_kw()?.value;
3285
3286            // Clause binders are BARE names — `Emit(token) -> { … }`. The types
3287            // live on the effect declaration; repeating them here would let the
3288            // two disagree.
3289            self.consume(TokenType::LParen)?;
3290            let mut parameter_names = Vec::new();
3291            while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
3292                parameter_names.push(self.consume_any_ident_or_kw()?.value);
3293                if self.check(TokenType::Comma) {
3294                    self.advance();
3295                }
3296            }
3297            self.consume(TokenType::RParen)?;
3298            self.consume(TokenType::Arrow)?;
3299            self.consume(TokenType::LBrace)?;
3300
3301            let mut body = Vec::new();
3302            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3303                body.push(self.parse_flow_step()?);
3304            }
3305            self.consume(TokenType::RBrace)?;
3306
3307            if let Some(prior) = clauses.iter().find(|c| c.operation_name == operation_name) {
3308                return Err(ParseError {
3309                    message: format!(
3310                        "handler declares clause `{operation_name}` twice (first at line {}); \
3311                         dispatch finds a clause by operation NAME and would always run the \
3312                         first, leaving the second dead",
3313                        prior.loc.line
3314                    ),
3315                    line: clause_tok.line,
3316                    column: clause_tok.column,
3317                    ..Default::default()
3318                });
3319            }
3320
3321            clauses.push(HandlerClause {
3322                operation_name,
3323                parameter_names,
3324                body,
3325                loc: self.loc_of(&clause_tok),
3326            });
3327        }
3328        self.consume(TokenType::RBrace)?;
3329
3330        // The `in { … }` delimiter is MANDATORY. A `handle` without it declares
3331        // a scope with no extent — nothing could ever be intercepted by it, and
3332        // accepting it would let an author believe an effect was handled when
3333        // no `perform` is inside anything.
3334        let in_tok = self.current().clone();
3335        if !self.check(TokenType::In) {
3336            return Err(ParseError {
3337                message: format!(
3338                    "`handle {}` must be followed by `in {{ … }}` — a handler scope is \
3339                     DELIMITED (the design plan D3). Without the `in` block the frame has no \
3340                     extent, so no `perform` could ever reach these clauses (got '{}')",
3341                    effect_names.join(", "),
3342                    in_tok.value
3343                ),
3344                line: in_tok.line,
3345                column: in_tok.column,
3346                ..Default::default()
3347            });
3348        }
3349        self.advance();
3350        self.consume(TokenType::LBrace)?;
3351        let mut body = Vec::new();
3352        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3353            body.push(self.parse_flow_step()?);
3354        }
3355        self.consume(TokenType::RBrace)?;
3356
3357        Ok(HandleBlock {
3358            effect_names,
3359            clauses,
3360            body,
3361            loc,
3362        })
3363    }
3364
3365    /// The shared head of `perform` and `forward` (D12): an optionally
3366    /// qualified operation name plus a parenthesised argument list.
3367    ///
3368    /// the design decision — BOTH spellings parse. `SSE.Emit(x)` fixes the effect here;
3369    /// `Emit(x)` leaves `effect_name` `None` and the closed catalog resolves it
3370    /// downstream, where an ambiguity can be reported with both candidates
3371    /// named. The qualified form is told from the bare one by the `.`, which
3372    /// cannot appear in an operation name.
3373    fn parse_effect_op_ref(
3374        &mut self,
3375    ) -> Result<(Option<String>, String, Vec<String>), ParseError> {
3376        let first = self.consume_any_ident_or_kw()?.value;
3377        let (effect_name, operation_name) = if self.check(TokenType::Dot) {
3378            self.advance();
3379            (Some(first), self.consume_any_ident_or_kw()?.value)
3380        } else {
3381            (None, first)
3382        };
3383
3384        self.consume(TokenType::LParen)?;
3385        let mut arguments = Vec::new();
3386        while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
3387            // v2.83.0 SUBJECTS: `the design plan` section 3.1 writes `perform
3388            // Emit(response.token)` — a dotted reference into a prior binding.
3389            arguments.push(self.parse_subject()?);
3390            if self.check(TokenType::Comma) {
3391                self.advance();
3392            }
3393        }
3394        self.consume(TokenType::RParen)?;
3395        Ok((effect_name, operation_name, arguments))
3396    }
3397
3398    /// `perform Emit(x)` / `perform SSE.Emit(x)`.
3399    fn parse_perform_step(&mut self) -> Result<PerformStep, ParseError> {
3400        let tok = self.consume(TokenType::Perform)?;
3401        let (effect_name, operation_name, arguments) = self.parse_effect_op_ref()?;
3402        Ok(PerformStep {
3403            effect_name,
3404            operation_name,
3405            arguments,
3406            loc: self.loc_of(&tok),
3407        })
3408    }
3409
3410    /// `forward Emit(t)` / `forward SSE.Emit(t)` (D12).
3411    fn parse_forward_step(&mut self) -> Result<ForwardStep, ParseError> {
3412        let tok = self.consume(TokenType::Forward)?;
3413        let (effect_name, operation_name, arguments) = self.parse_effect_op_ref()?;
3414        Ok(ForwardStep {
3415            effect_name,
3416            operation_name,
3417            arguments,
3418            loc: self.loc_of(&tok),
3419        })
3420    }
3421
3422    /// The shared body of `resume(…)` / `abort(…)`: an optional single value.
3423    fn parse_discharge_value(&mut self) -> Result<String, ParseError> {
3424        self.consume(TokenType::LParen)?;
3425        let value = if self.check(TokenType::RParen) {
3426            String::new()
3427        } else {
3428            self.parse_subject()?
3429        };
3430        self.consume(TokenType::RParen)?;
3431        Ok(value)
3432    }
3433
3434    fn parse_param_list(&mut self) -> Result<Vec<Parameter>, ParseError> {
3435        let mut params = Vec::new();
3436
3437        let name = self.consume(TokenType::Identifier)?;
3438        let ploc = self.loc_of(&name);
3439        self.consume(TokenType::Colon)?;
3440        let type_expr = self.parse_type_expr()?;
3441        params.push(Parameter {
3442            name: name.value,
3443            type_expr,
3444            loc: ploc,
3445        });
3446
3447        while self.check(TokenType::Comma) {
3448            self.advance();
3449            let name = self.consume(TokenType::Identifier)?;
3450            let ploc = self.loc_of(&name);
3451            self.consume(TokenType::Colon)?;
3452            let type_expr = self.parse_type_expr()?;
3453            params.push(Parameter {
3454                name: name.value,
3455                type_expr,
3456                loc: ploc,
3457            });
3458        }
3459        Ok(params)
3460    }
3461
3462    // ── FLOW STEP dispatch ───────────────────────────────────────
3463
3464    fn parse_flow_step(&mut self) -> Result<FlowStep, ParseError> {
3465        let tok = self.current().clone();
3466
3467        match tok.ttype {
3468            // v2.83.0 — an epistemic block INSIDE a flow body. Its
3469            // children are hoisted to program level (see `Parser::hoisted`),
3470            // which is exactly what a top-level block already does, so the
3471            // nested spelling costs nothing downstream. The flow itself gets
3472            // no node: the block declares, it does not execute.
3473            TokenType::Know | TokenType::Believe | TokenType::Speculate
3474                if self
3475                    .tokens
3476                    .get(self.pos + 1)
3477                    .is_some_and(|t| t.ttype == TokenType::LBrace) =>
3478            {
3479                let block = self.parse_epistemic_block()?;
3480                self.hoisted.push(Declaration::Epistemic(block));
3481                self.parse_flow_step()
3482            }
3483            TokenType::Doubt
3484                if self
3485                    .tokens
3486                    .get(self.pos + 1)
3487                    .is_some_and(|t| t.ttype == TokenType::LBrace) =>
3488            {
3489                let block = self.parse_epistemic_block()?;
3490                self.hoisted.push(Declaration::Epistemic(block));
3491                self.parse_flow_step()
3492            }
3493            TokenType::Step => self.parse_step().map(FlowStep::Step),
3494            TokenType::If => self.parse_if().map(FlowStep::If),
3495            TokenType::For => self.parse_for_in().map(FlowStep::ForIn),
3496            TokenType::Let => self.parse_let().map(FlowStep::Let),
3497            TokenType::Return => self.parse_return().map(FlowStep::Return),
3498            TokenType::Break => self.parse_break().map(FlowStep::Break),
3499            TokenType::Continue => self.parse_continue().map(FlowStep::Continue),
3500            TokenType::Lambda => self.parse_lambda_data_apply().map(FlowStep::LambdaDataApply),
3501
3502            // ── Tier 2 flow steps (typed AST) ─────────────────────
3503            TokenType::Probe => self.parse_flow_step_simple("probe").map(|l| FlowStep::Probe(ProbeStep { target: l.1, fields: Vec::new(), loc: l.0 })),
3504            // v2.83.0 — ONE implementation for both positions (the the design decision
3505            // doctrine). `reason <target>` and `reason { given ask depth }` are
3506            // the same node; the second is what the README publishes.
3507            TokenType::Reason => self.parse_reason_step().map(FlowStep::Reason),
3508            TokenType::Validate => self.parse_flow_step_simple("validate").map(|l| FlowStep::Validate(ValidateStep { target: l.1, rule: String::new(), guard: None, loc: l.0 })),
3509            TokenType::Refine => self.parse_flow_step_simple("refine").map(|l| FlowStep::Refine(RefineStep { target: l.1, strategy: String::new(), loc: l.0 })),
3510            TokenType::Weave => self.parse_weave_step(),
3511            TokenType::Use => self.parse_use_step(),
3512            TokenType::Remember => self.parse_remember_step(),
3513            TokenType::Recall => self.parse_recall_step(),
3514            TokenType::Par => self.parse_par_block().map(FlowStep::Par),
3515            TokenType::Hibernate => self.parse_hibernate_step(),
3516            TokenType::Deliberate => self.parse_block_step("deliberate").map(|l| FlowStep::Deliberate(DeliberateBlock { loc: l })),
3517            TokenType::Consensus => self.parse_block_step("consensus").map(|l| FlowStep::Consensus(ConsensusBlock { loc: l })),
3518            TokenType::Forge => self.parse_forge_step().map(FlowStep::Forge),
3519            TokenType::Focus => self.parse_focus_step(),
3520            TokenType::Grad => self.parse_grad_step(),
3521            TokenType::Associate => self.parse_associate_step(),
3522            TokenType::Aggregate => self.parse_aggregate_step(),
3523            TokenType::Explore => self.parse_explore_step(),
3524            TokenType::Ingest => self.parse_ingest_step(),
3525            TokenType::Shield => self.parse_apply_step("shield").map(|l| FlowStep::ShieldApply(ShieldApplyStep { shield_name: l.1, target: l.2, output_type: l.3, loc: l.0 })),
3526            // v2.67.0 — `stream` parses its BODY. It used to go through
3527            // `parse_block_step`, whose entire job is `skip_braced_block()` —
3528            // the block's contents were thrown away at parse time, which is why
3529            // `run_stream` had nothing to run and "completed" with an empty
3530            // string while the README sold "Algebraic Effects and Free Monads".
3531            TokenType::Stream => self.parse_stream_block().map(FlowStep::Stream),
3532            // ── v2.87.0 — algebraic effects ────────────────────
3533            //
3534            // All five constructs parse at flow level. `resume` / `abort` /
3535            // `forward` are legal only inside a handler CLAUSE — that scope law
3536            // is enforced by the type-checker (v2.87.0), not here, because the
3537            // parser does not know whether an enclosing `handle` exists when it
3538            // is re-entered through `parse_flow_step` from a clause body.
3539            TokenType::Handle => self.parse_handle_block().map(FlowStep::Handle),
3540            TokenType::Perform => self.parse_perform_step().map(FlowStep::Perform),
3541            TokenType::Resume => {
3542                let tok = self.consume(TokenType::Resume)?;
3543                let value_expr = self.parse_discharge_value()?;
3544                Ok(FlowStep::Resume(ResumeStep {
3545                    value_expr,
3546                    loc: self.loc_of(&tok),
3547                }))
3548            }
3549            TokenType::Abort => {
3550                let tok = self.consume(TokenType::Abort)?;
3551                let value_expr = self.parse_discharge_value()?;
3552                Ok(FlowStep::Abort(AbortStep {
3553                    value_expr,
3554                    loc: self.loc_of(&tok),
3555                }))
3556            }
3557            TokenType::Forward => self.parse_forward_step().map(FlowStep::Forward),
3558            TokenType::Navigate => self.parse_navigate_step(),
3559            TokenType::Drill => self.parse_drill_step(),
3560            TokenType::Trail => self.parse_flow_step_simple("trail").map(|l| FlowStep::Trail(TrailStep { navigate_ref: l.1, loc: l.0 })),
3561            TokenType::Corroborate => self.parse_corroborate_step(),
3562            TokenType::Ots => self.parse_apply_step("ots").map(|l| FlowStep::OtsApply(OtsApplyStep { ots_name: l.1, target: l.2, output_type: l.3, loc: l.0 })),
3563            TokenType::Mandate => self.parse_apply_step("mandate").map(|l| FlowStep::MandateApply(MandateApplyStep { mandate_name: l.1, target: l.2, output_type: l.3, loc: l.0 })),
3564            // v2.67.0 — `compute <Name> on a, b -> out`. The ARGUMENTS used to
3565            // be `Vec::new()` — hardcoded empty at the parse site — so even if
3566            // the runtime had wanted to compute something, it had nothing to
3567            // compute it FROM.
3568            TokenType::Compute => self.parse_compute_apply().map(FlowStep::ComputeApply),
3569            TokenType::Listen => self.parse_listen_step(),
3570            TokenType::Daemon => self.parse_flow_step_simple("daemon").map(|l| FlowStep::DaemonStep(DaemonStepNode { daemon_ref: l.1, loc: l.0 })),
3571            // v1.6.0 — Mobile typed channels (paper section 3.1, section 3.2, section 4.3)
3572            TokenType::Emit => self.parse_emit_step(),
3573            // v2.46.0 — `mint <Credential> as <binding>` (ephemeral credential).
3574            TokenType::Mint => self.parse_mint_step(),
3575            // v2.48.0 — `rotate <SecretsStore> [where "…"] with <Tool> as
3576            // <binding>` (mediated secret renewal).
3577            TokenType::Rotate => self.parse_rotate_step(),
3578            TokenType::Publish => self.parse_publish_step(),
3579            TokenType::Discover => self.parse_discover_step(),
3580            TokenType::Persist => self.parse_persist_step(),
3581            TokenType::Retrieve => self.parse_retrieve_step(),
3582            TokenType::Mutate => self.parse_mutate_step(),
3583            TokenType::Purge => self.parse_store_where_step().map(|(loc, store_name, where_expr)| FlowStep::Purge(PurgeStep { store_name, where_expr, loc })),
3584            TokenType::Transact => self.parse_block_step("transact").map(|l| FlowStep::Transact(TransactBlock { loc: l })),
3585            // v2.43.0 — the `warden` adversarial-analysis block.
3586            TokenType::Warden => self.parse_warden().map(FlowStep::Warden),
3587            // v2.4.0 — the `quant` cognitive block (Hilbert-space projection).
3588            TokenType::Quant => self.parse_quant().map(FlowStep::Quant),
3589            // v2.4.0 — the `yield` measurement point.
3590            TokenType::Yield => self.parse_yield().map(FlowStep::Yield),
3591            // v2.4.0 — `run <Flow>(args)` as a flow-step: invoke a declared
3592            // flow from inside a body (a `daemon` listen handler, Q3). Reuses
3593            // the top-level run parser.
3594            TokenType::Run => self.parse_run().map(FlowStep::Run),
3595
3596            _ => {
3597                // v1.20.0 — append "Did you mean X?" hint when the
3598                // unknown token looks like a typo'd flow-body keyword
3599                // (e.g. `stepp` / `reasn` / `validte`). D3, D11.
3600                let hint = crate::smart_suggest::suggest_for(
3601                    &tok.value,
3602                    crate::smart_suggest::FLOW_BODY_KEYWORD_NAMES,
3603                );
3604                let base = format!(
3605                    "Unexpected token in flow body: '{}' — expected step, if, for, let, return, ...",
3606                    tok.value
3607                );
3608                let message = if hint.is_empty() {
3609                    base
3610                } else {
3611                    format!("{base}. {hint}")
3612                };
3613                Err(ParseError {
3614                    message,
3615                    line: tok.line,
3616                    column: tok.column,
3617                    ..Default::default()
3618                })
3619            }
3620        }
3621    }
3622
3623    // ── STEP ─────────────────────────────────────────────────────
3624
3625    fn parse_step(&mut self) -> Result<StepNode, ParseError> {
3626        let tok = self.consume(TokenType::Step)?;
3627        let loc = self.loc_of(&tok);
3628        let name = self.consume(TokenType::Identifier)?.value;
3629
3630        let mut persona_ref = String::new();
3631        if self.check(TokenType::Use) {
3632            self.advance();
3633            persona_ref = self.consume_any_ident_or_kw()?.value;
3634        }
3635
3636        self.consume(TokenType::LBrace)?;
3637
3638        let mut node = StepNode {
3639            name,
3640            persona_ref,
3641            given: String::new(),
3642            ask: String::new(),
3643            output_type: String::new(),
3644            confidence_floor: None,
3645            navigate_ref: String::new(),
3646            apply_ref: String::new(),
3647            requires_context: None,
3648            now_tz: None,
3649            guards: Vec::new(),
3650            pix_ops: Vec::new(),
3651            stream: None,
3652            performs: Vec::new(),
3653            loc,
3654        };
3655
3656        self.parse_step_body_into(&mut node)?;
3657        self.consume(TokenType::RBrace)?;
3658        Ok(node)
3659    }
3660
3661    /// v2.83.0 — the step-body field/statement loop, extracted from
3662    /// [`Self::parse_step`] so a `stream<T>` handler arm can reuse it VERBATIM.
3663    ///
3664    /// The caller has already consumed the opening `{` and owns the closing `}`.
3665    ///
3666    /// Extracting it is what keeps `on_chunk: { … }` honest. The published arm
3667    /// body is a STEP body — `probe chunk for […]` followed by
3668    /// `output: QuoteSnapshot` — and `output:` has no flow-level position, so
3669    /// parsing the arm as a flow body would have rejected the README's own
3670    /// example. Re-implementing the loop instead would fork the grammar: every
3671    /// future step-body statement would have to be added twice, and the second
3672    /// copy is the one that rots.
3673    fn parse_step_body_into(&mut self, node: &mut StepNode) -> Result<(), ParseError> {
3674        while !self.check(TokenType::RBrace) {
3675            let inner = self.current().clone();
3676
3677            match inner.ttype {
3678                TokenType::Given => {
3679                    self.advance();
3680                    self.consume(TokenType::Colon)?;
3681                    node.given = self.parse_expression_string()?;
3682                }
3683                TokenType::Ask => {
3684                    self.advance();
3685                    self.consume(TokenType::Colon)?;
3686                    node.ask = self.consume(TokenType::StringLit)?.value;
3687                }
3688                TokenType::Output => {
3689                    // Mirror of Python `_parse_step` `case "output":`
3690                    // which uses `_parse_output_type_string` — accepts
3691                    // the FULL generic-aware shape `Stream<T>`,
3692                    // `Stream<T>?`, `Identifier?`, NOT just the bare
3693                    // head identifier. Pre-fix the step parser dropped
3694                    // `<T>` and downstream `flow_has_stream_output`'s
3695                    // `starts_with("Stream<") && ends_with('>')` then
3696                    // returned false → `implicit_transport == "json"`
3697                    // → dynamic routes served JSON instead of SSE.
3698                    self.advance();
3699                    self.consume(TokenType::Colon)?;
3700                    node.output_type = self.parse_output_type_string()?;
3701                }
3702                // v2.83.0 — `navigate` in a step body is TWO forms, told
3703                // apart by the token after the keyword:
3704                // `navigate: <Ref>` the field (pre-v2.83.0)
3705                //   `navigate <Ref> query: …` the STATEMENT README publishes
3706                // The second is an elevation: it binds `as:` before the step
3707                // generates, so the step's `ask:` can interpolate it.
3708                TokenType::Navigate
3709                    if self
3710                        .tokens
3711                        .get(self.pos + 1)
3712                        .is_some_and(|t| t.ttype != TokenType::Colon) =>
3713                {
3714                    let op = self.parse_navigate_step()?;
3715                    node.pix_ops.push(op);
3716                }
3717                TokenType::Drill => {
3718                    let op = self.parse_drill_step()?;
3719                    node.pix_ops.push(op);
3720                }
3721                TokenType::Trail => {
3722                    let op = self
3723                        .parse_flow_step_simple("trail")
3724                        .map(|l| FlowStep::Trail(TrailStep { navigate_ref: l.1, loc: l.0 }))?;
3725                    node.pix_ops.push(op);
3726                }
3727                // v2.83.0 — `validate <binding> against: <Schema>`, the
3728                // form README's pix family publishes inside a step. The
3729                // flow-level `validate <target>` already exists; this adds the
3730                // step position plus the `against:` clause the docs write.
3731                TokenType::Validate => {
3732                    let tok = self.current().clone();
3733                    self.advance();
3734                    // v2.83.0 — SUBJECT: `validate Assess.output against: X`.
3735                    let target = self.parse_subject()?;
3736                    let mut rule = String::new();
3737                    if self.current().value == "against" {
3738                        self.advance();
3739                        self.consume(TokenType::Colon)?;
3740                        rule = self.consume_any_ident_or_kw()?.value.clone();
3741                    }
3742                    node.pix_ops.push(FlowStep::Validate(ValidateStep {
3743                        target,
3744                        rule,
3745                        guard: None,
3746                        loc: Loc { line: tok.line, column: tok.column },
3747                    }));
3748                }
3749                // v2.88.0 — `if confidence < 0.8 -> refine(max_attempts: 2)`,
3750                // the self-correction guard blocks 1/16/18 publish immediately
3751                // after a `validate … against:`.
3752                //
3753                // Every position in the form is a CLOSED catalog of one — the
3754                // metric (`confidence`), the comparison (`<`), the action
3755                // (`refine`), the argument (`max_attempts`) — and each refusal
3756                // below names its catalog, because a free position here would
3757                // breed the imaginary catalog three cycles have now paid for.
3758                // General branching (`if <cond> { … } else { … }`) stays a
3759                // FLOW-level construct; a step body gets a guard or nothing.
3760                TokenType::If => {
3761                    let tok = self.current().clone();
3762                    self.advance();
3763
3764                    let metric = self.consume_any_ident_or_kw()?;
3765                    if metric.value != "confidence" {
3766                        return Err(ParseError {
3767                            message: format!(
3768                                "step-body `if` is the confidence guard — `if confidence < \
3769                                 <threshold> -> refine(max_attempts: <n>)` — and `confidence` \
3770                                 is its only metric (the CSR the preceding `validate … \
3771                                 against:` computes). Got '{}'. General branching belongs at \
3772                                 flow level: `if <cond> {{ … }}`.",
3773                                metric.value
3774                            ),
3775                            line: metric.line,
3776                            column: metric.column,
3777                            ..Default::default()
3778                        });
3779                    }
3780
3781                    let op = self.current().clone();
3782                    if op.ttype != TokenType::Lt {
3783                        return Err(ParseError {
3784                            message: format!(
3785                                "a confidence guard declares a FLOOR: `if confidence < \
3786                                 <threshold>`. `<` is the only comparison — the guard fires on \
3787                                 DEFICIENCY, and an inverted form would refine the outputs \
3788                                 that already conform. Got '{}'.",
3789                                op.value
3790                            ),
3791                            line: op.line,
3792                            column: op.column,
3793                            ..Default::default()
3794                        });
3795                    }
3796                    self.advance();
3797                    let threshold = self.consume_number()?;
3798
3799                    self.consume(TokenType::Arrow)?;
3800
3801                    if !self.check(TokenType::Refine) {
3802                        let bad = self.current().clone();
3803                        return Err(ParseError {
3804                            message: format!(
3805                                "the guard's action catalog is CLOSED and `refine` is its only \
3806                                 member — the recovery the runtime actually performs (re-derive \
3807                                 the validated value with the violations as feedback, then \
3808                                 re-score). Got '{}'. An action name outside the catalog would \
3809                                 advertise a recovery nothing dispatches.",
3810                                bad.value
3811                            ),
3812                            line: bad.line,
3813                            column: bad.column,
3814                            ..Default::default()
3815                        });
3816                    }
3817                    self.advance();
3818                    self.consume(TokenType::LParen)?;
3819                    let key = self.consume_any_ident_or_kw()?;
3820                    if key.value != "max_attempts" {
3821                        return Err(ParseError {
3822                            message: format!(
3823                                "`refine` takes exactly `max_attempts: <n>` — the bound that \
3824                                 makes the recovery loop TERMINATE by construction. Got '{}'.",
3825                                key.value
3826                            ),
3827                            line: key.line,
3828                            column: key.column,
3829                            ..Default::default()
3830                        });
3831                    }
3832                    self.consume(TokenType::Colon)?;
3833                    let attempts_tok = self.current().clone();
3834                    if attempts_tok.ttype != TokenType::Integer {
3835                        return Err(ParseError {
3836                            message: format!(
3837                                "`max_attempts:` must be a positive integer literal (got '{}')",
3838                                attempts_tok.value
3839                            ),
3840                            line: attempts_tok.line,
3841                            column: attempts_tok.column,
3842                            ..Default::default()
3843                        });
3844                    }
3845                    let max_attempts = attempts_tok.value.parse::<u32>().map_err(|_| ParseError {
3846                        message: format!("Invalid attempt count '{}'", attempts_tok.value),
3847                        line: attempts_tok.line,
3848                        column: attempts_tok.column,
3849                        ..Default::default()
3850                    })?;
3851                    self.advance();
3852                    self.consume(TokenType::RParen)?;
3853
3854                    // ATTACH to the validation this guard governs: the nearest
3855                    // preceding `validate … against:` in THIS step body. The
3856                    // attachment is what makes `confidence` unambiguous by
3857                    // construction — see `ast::ValidateStep::guard`. No such
3858                    // validation ⇒ the guard has nothing to read, and a guard
3859                    // over a score nobody computed is governance theatre.
3860                    let attached = node.pix_ops.iter_mut().rev().find_map(|op| match op {
3861                        FlowStep::Validate(v) if !v.rule.is_empty() => Some(v),
3862                        _ => None,
3863                    });
3864                    match attached {
3865                        Some(v) => {
3866                            if v.guard.is_some() {
3867                                return Err(ParseError {
3868                                    message: "this validation already carries a confidence \
3869                                              guard; a second one would race the first over \
3870                                              the same score. One validation, one floor, one \
3871                                              recovery."
3872                                        .to_string(),
3873                                    line: tok.line,
3874                                    column: tok.column,
3875                                    ..Default::default()
3876                                });
3877                            }
3878                            v.guard = Some(ConfidenceGuard {
3879                                threshold,
3880                                max_attempts,
3881                                loc: Loc { line: tok.line, column: tok.column },
3882                            });
3883                        }
3884                        None => {
3885                            return Err(ParseError {
3886                                message: "`if confidence` reads the CSR of a preceding \
3887                                          `validate … against: <Schema>` in this step body, \
3888                                          and none exists. A `validate` without `against:` \
3889                                          computes no score (there is no schema to score \
3890                                          with), so it cannot carry a guard either."
3891                                    .to_string(),
3892                                line: tok.line,
3893                                column: tok.column,
3894                                ..Default::default()
3895                            });
3896                        }
3897                    }
3898                }
3899                TokenType::Navigate => {
3900                    self.advance();
3901                    self.consume(TokenType::Colon)?;
3902                    node.navigate_ref = self.parse_dotted_identifier()?;
3903                }
3904                TokenType::Identifier if inner.value == "confidence_floor" => {
3905                    self.advance();
3906                    self.consume(TokenType::Colon)?;
3907                    node.confidence_floor = Some(self.consume_number()?);
3908                }
3909                TokenType::Identifier if inner.value == "apply" => {
3910                    self.advance();
3911                    self.consume(TokenType::Colon)?;
3912                    node.apply_ref = self.consume_any_ident_or_kw()?.value;
3913                }
3914                // v2.22.0 — `requires_context: <tokens>`: the step's declared
3915                // model-capability requirement (the context window the cognition
3916                // needs). A bare positive integer literal; the v2.22.0 resolver maps
3917                // it to a concrete model. Range/ceiling is the type-checker's job
3918                // (v2.22.0 positive-int + v2.22.0 catalog ceiling) — the parser only
3919                // requires an integer token here (a float / non-number is a parse
3920                // error, surfaced at the exact column).
3921                TokenType::Identifier if inner.value == "requires_context" => {
3922                    self.advance();
3923                    self.consume(TokenType::Colon)?;
3924                    let num = self.current().clone();
3925                    let bad = |tok: &crate::tokens::Token| ParseError {
3926                        message: format!(
3927                            "`requires_context:` must be a positive integer token count \
3928                             (got '{}')",
3929                            tok.value
3930                        ),
3931                        line: tok.line,
3932                        column: tok.column,
3933                        ..Default::default()
3934                    };
3935                    if num.ttype != TokenType::Integer {
3936                        return Err(bad(&num));
3937                    }
3938                    let value = num.value.parse::<u32>().map_err(|_| bad(&num))?;
3939                    self.advance();
3940                    node.requires_context = Some(value);
3941                }
3942                // v2.46.0 — `now: "<IANA-tz>"`: the step's declared cognitive
3943                // timezone. A string literal; the format law (IANA shape) is the
3944                // type-checker's job (`axon-T892`) — the parser only requires a
3945                // string token here, surfaced at the exact column.
3946                TokenType::Identifier if inner.value == "now" => {
3947                    self.advance();
3948                    self.consume(TokenType::Colon)?;
3949                    let tz = self.current().clone();
3950                    if tz.ttype != TokenType::StringLit {
3951                        return Err(ParseError {
3952                            message: format!(
3953                                "`now:` must be an IANA timezone string literal like \
3954                                 \"America/Bogota\" or \"UTC\" (got '{}')",
3955                                tz.value
3956                            ),
3957                            line: tz.line,
3958                            column: tz.column,
3959                            ..Default::default()
3960                        });
3961                    }
3962                    self.advance();
3963                    node.now_tz = Some(tz.value);
3964                }
3965                // v2.7.0 — a `use` nested inside a `step { }` body used
3966                // to be skipped structurally (grouped with the sub-constructs
3967                // below), silently degrading the tool dispatch to an
3968                // unconstrained LLM step with NO diagnostic. That fallthrough
3969                // drops the AST node before the type-checker can see it, so the
3970                // resource the tool would provision is never linearly accounted
3971                // for (use_tool soundness). Reject it here, at the parser —
3972                // the only place that still sees the token — and redirect to
3973                // the canonical forms.
3974                TokenType::Use => {
3975                    let tool = self
3976                        .tokens
3977                        .get(self.pos + 1)
3978                        .map(|t| t.value.as_str())
3979                        .filter(|v| !v.is_empty())
3980                        .unwrap_or("<Tool>");
3981                    return Err(ParseError {
3982                        message: format!(
3983                            "`use` is not valid inside a `step {{ }}` body — the tool dispatch \
3984                             would be silently dropped. To invoke a tool, either write the \
3985                             flow-level step `use {tool} on <arg>` (outside this block), or bind \
3986                             it inside this step with `apply: {tool}`. To attach a persona, put \
3987                             it in the step header: `step <name> use <Persona> {{ … }}`."
3988                        ),
3989                        line: inner.line,
3990                        column: inner.column,
3991                        ..Default::default()
3992                    });
3993                }
3994                // v2.83.0 — `mandate X on Y`, `shield X on Y -> b`,
3995                // `ots X on Y` as STEP-BODY statements. README XV has always
3996                // written the application here — next to the `output:` it
3997                // constrains — and the parser accepted the same form only at
3998                // flow level, which is why README blocks 40–42 never compiled.
3999                // The published position is also the better semantics: a
4000                // mandate inside a step is scoped to THIS step's generation;
4001                // the flow-level form governs a bare statement whose subject
4002                // must be inferred. One concept, two positions, same AST shape
4003                // as the flow-level `*ApplyStep` family.
4004                TokenType::Mandate => {
4005                    let g = self.parse_step_guard("mandate")?;
4006                    node.guards.push(g);
4007                }
4008                TokenType::Shield => {
4009                    let g = self.parse_step_guard("shield")?;
4010                    node.guards.push(g);
4011                }
4012                TokenType::Ots => {
4013                    let g = self.parse_step_guard("ots")?;
4014                    node.guards.push(g);
4015                }
4016                // v2.83.0 — `lambda RawQuote on ticker -> verified_quote`
4017                // inside a step body: README blocks 46-47's exact shape, the
4018                // the design decision statement position extended to the fourth member of
4019                // the apply family. Semantically it is an ELEVATION, not a
4020                // guard: dispatch runs it BEFORE the step's generation, so the
4021                // elevated binding is in scope for the prompt.
4022                TokenType::Lambda => {
4023                    let g = self.parse_step_guard("lambda")?;
4024                    node.guards.push(g);
4025                }
4026                // v2.83.0 — `probe <target> for [a, b, c]` as a STATEMENT.
4027                //
4028                // `probe` used to fall into `skip_flow_step_structural` below,
4029                // which DISCARDED it — the v2.67.0 silent-drop shape, in the step
4030                // parser. The extraction list had nowhere to live even at flow
4031                // level. Both are fixed here: the statement is kept, and its
4032                // `for [...]` list reaches the AST.
4033                TokenType::Probe
4034                    if self
4035                        .tokens
4036                        .get(self.pos + 1)
4037                        .is_some_and(|t| t.ttype != TokenType::Colon) =>
4038                {
4039                    let tok = self.current().clone();
4040                    self.advance();
4041                    // v2.83.0 — SUBJECT: README psyche writes
4042                    // `probe student.recent_interactions for [...]`.
4043                    let target = self.parse_subject()?;
4044                    let mut fields = Vec::new();
4045                    if self.check(TokenType::For) {
4046                        self.advance();
4047                        self.consume(TokenType::LBracket)?;
4048                        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
4049                            fields.push(self.consume_any_ident_or_kw()?.value.clone());
4050                            if self.check(TokenType::Comma) {
4051                                self.advance();
4052                            }
4053                        }
4054                        self.consume(TokenType::RBracket)?;
4055                    }
4056                    node.pix_ops.push(FlowStep::Probe(ProbeStep {
4057                        target,
4058                        fields,
4059                        loc: Loc { line: tok.line, column: tok.column },
4060                    }));
4061                }
4062                // v2.83.0 — `use_tool <name> [with k: v, …]` as a STATEMENT.
4063                // v2.7.0 made `use` inside a step body a hard error pointing at
4064                // the canonical forms; `use_tool` is the OTHER spelling README
4065                // publishes, and it names the tool explicitly, so there is no
4066                // ambiguity to protect against — the dispatch is not dropped,
4067                // it is recorded.
4068                TokenType::Identifier if inner.value == "use_tool" => {
4069                    let tok = self.current().clone();
4070                    self.advance();
4071                    let tool_name = self.consume_any_ident_or_kw()?.value.clone();
4072                    let args = if self.current().value == "with" {
4073                        self.advance();
4074                        let mut named: Vec<(String, String, String)> = Vec::new();
4075                        loop {
4076                            let k = self.consume_any_ident_or_kw()?.value.clone();
4077                            self.consume(TokenType::Colon)?;
4078                            // `value_kind` mirrors v2.10.0's classification: a
4079                            // string literal is a literal, anything else is a
4080                            // binding reference the runtime must look up.
4081                            let kind = if self.check(TokenType::StringLit) {
4082                                "literal"
4083                            } else {
4084                                "reference"
4085                            };
4086                            let v = self.parse_expression_string()?;
4087                            named.push((k, v, kind.to_string()));
4088                            if self.check(TokenType::Comma) {
4089                                self.advance();
4090                            } else {
4091                                break;
4092                            }
4093                        }
4094                        UseArgs::Named(named)
4095                    } else if self.current().value == "on" {
4096                        self.advance();
4097                        UseArgs::LegacyPositional(
4098                            self.consume_any_ident_or_kw()?.value.clone(),
4099                        )
4100                    } else {
4101                        UseArgs::LegacyPositional(String::new())
4102                    };
4103                    node.pix_ops.push(FlowStep::UseTool(UseToolStep {
4104                        tool_name,
4105                        args,
4106                        loc: Loc { line: tok.line, column: tok.column },
4107                    }));
4108                }
4109                // v2.83.0 — `par { … }` inside a step body.
4110                TokenType::Par => {
4111                    let block = self.parse_par_block()?;
4112                    node.pix_ops.push(FlowStep::Par(block));
4113                }
4114                // v2.83.0 — `reason { given: … ask: "…" depth: N }` as a
4115                // step-body statement. This is the README's single most-published
4116                // cognitive form (16 blocks) and it was the most expensive
4117                // resident of the silent-drop arm below: the block reached
4118                // `skip_flow_step_structural`, which discarded it, so a step
4119                // whose ONLY cognition was a `reason` lowered to an empty `ask`
4120                // and generated over nothing. The elevation position and the
4121                // flow position share `parse_reason_step` — one concept, two
4122                // positions.
4123                // v2.83.0 — `reason` in a step body is TWO forms, told
4124                // apart by the token after the keyword, exactly as v2.83.0 did
4125                // for `navigate`:
4126                //
4127                //   `reason: "…"`             the FIELD — a one-line deliberation
4128                //   `reason { given ask … }`  the STATEMENT README publishes
4129                //
4130                // The field form was already written across this repo's own
4131                // fixtures and it did NOTHING: `skip_flow_step_structural`
4132                // swallowed the key AND its value. Reading it as a `reason`
4133                // whose `ask:` is that value is not new semantics — it is the
4134                // block form with one field, which is what the line says.
4135                TokenType::Reason
4136                    if self
4137                        .tokens
4138                        .get(self.pos + 1)
4139                        .is_some_and(|t| t.ttype == TokenType::Colon) =>
4140                {
4141                    let tok = self.current().clone();
4142                    self.advance();
4143                    self.consume(TokenType::Colon)?;
4144                    let mut r = ReasonStep {
4145                        strategy: String::new(),
4146                        target: String::new(),
4147                        given: String::new(),
4148                        ask: String::new(),
4149                        depth: None,
4150                        loc: self.loc_of(&tok),
4151                    };
4152                    if self.check(TokenType::StringLit) {
4153                        r.ask = self.consume(TokenType::StringLit)?.value;
4154                    } else {
4155                        r.target = self.parse_dotted_identifier()?;
4156                    }
4157                    node.pix_ops.push(FlowStep::Reason(r));
4158                }
4159                TokenType::Reason => {
4160                    let r = self.parse_reason_step()?;
4161                    node.pix_ops.push(FlowStep::Reason(r));
4162                }
4163                // v2.83.0 — `weave [a, b] format: T include: […]` as a
4164                // step-body statement: the shape fourteen README blocks close
4165                // with. It was the worst resident of the silent-drop arm below,
4166                // because it did not merely lose the node — the skipper stops
4167                // at the first `output` KEYWORD it meets, so
4168                // `weave [A.output, B.output]` left the parser mid-list and the
4169                // step then failed with `Expected Colon` pointing at the comma.
4170                // A dropped construct AND a mislocated error.
4171                TokenType::Weave => {
4172                    let w = self.parse_weave_step()?;
4173                    node.pix_ops.push(w);
4174                }
4175                // v2.83.0 — `<Agent>(arg, …)` as a step-body statement:
4176                // the form every agent example in the README uses, and the one
4177                // that makes v2.83.0's executor reachable from source.
4178                //
4179                // Told apart from the field arms above by the `(` — those all
4180                // match on a specific field NAME, so a call can never shadow
4181                // one. The name is a NAME (never dotted: an agent declaration
4182                // has no path), the arguments are v2.83.0 SUBJECTS, because
4183                // README writes `TrendAnalyzer(Gather.output)`.
4184                TokenType::Identifier
4185                    if self
4186                        .tokens
4187                        .get(self.pos + 1)
4188                        .is_some_and(|t| t.ttype == TokenType::LParen) =>
4189                {
4190                    let tok = self.current().clone();
4191                    let agent_name = self.consume_any_ident_or_kw()?.value.clone();
4192                    self.consume(TokenType::LParen)?;
4193                    let mut arguments = Vec::new();
4194                    while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
4195                        arguments.push(self.parse_subject()?);
4196                        if self.check(TokenType::Comma) {
4197                            self.advance();
4198                        }
4199                    }
4200                    self.consume(TokenType::RParen)?;
4201                    node.pix_ops.push(FlowStep::AgentCall(AgentCallStep {
4202                        agent_name,
4203                        arguments,
4204                        loc: self.loc_of(&tok),
4205                    }));
4206                }
4207                // v2.83.0 — `retrieve from <Store> where "…"` as a
4208                // step-body statement. README axonstore writes the store read
4209                // INSIDE the step that consumes it, which is the elevation
4210                // position: the rows must be bound before the step generates.
4211                //
4212                // Unlike the three before it, this one needed no engine work —
4213                // `FlowStep::Retrieve` and `wire_integrations::run_retrieve`
4214                // are among the most-exercised paths in the system (v1.30.0–v1.31.0, the
4215                // pg integration suites). Only the position was missing.
4216                TokenType::Retrieve => {
4217                    let r = self.parse_retrieve_step()?;
4218                    node.pix_ops.push(r);
4219                }
4220                // v2.83.0 — `stream<T> { on_chunk: … on_complete: … }` in a
4221                // step body. THE LAST RESIDENT of the silent-drop arm leaves
4222                // here: `probe` left in v2.83.0, `reason` in v2.83.0, `weave` in
4223                // v2.83.0, `retrieve` in v2.83.0.
4224                //
4225                // What it cost, measured on README block 15 before this landed:
4226                // the whole block — a `probe`, a `validate`, and BOTH `output:`
4227                // declarations — went to `skip_flow_step_structural`, so
4228                // `step Stream` reached the dispatcher with `pix_ops=0`,
4229                // `ask=""`, `output=""`. An entirely EMPTY step, that `axon
4230                // check` passed with 0 errors, and whose `Stream.output` the
4231                // next step then reasoned over. The block had left the v2.81.0
4232                // ledger on the strength of compiling.
4233                //
4234                // NOT a `pix_ops` push — see `StepNode::stream`. The other ten
4235                // statements are elevations that run BEFORE generation; a stream
4236                // handler runs DURING it, and this step's output IS the stream.
4237                TokenType::Stream => {
4238                    let sb = self.parse_stream_block()?;
4239                    if node.stream.is_some() {
4240                        return Err(ParseError {
4241                            message:
4242                                "step declares two `stream` blocks; a step has one output stream, \
4243                                 and composing two has no defined meaning (which one is the \
4244                                 step's output?). Refused rather than silently keeping the last."
4245                                    .to_string(),
4246                            line: inner.line,
4247                            column: inner.column,
4248                            ..Default::default()
4249                        });
4250                    }
4251                    node.stream = Some(Box::new(sb));
4252                }
4253                // v2.87.0 — `perform Op(args)` in a step body, the position
4254                // `the design plan` section 3.1 publishes:
4255                //
4256                //     step generate {
4257                //         given: prompt
4258                //         perform Emit(response.token)
4259                //         perform Done()
4260                //     }
4261                //
4262                // NOT a `pix_ops` push, and this is the v2.83.0 lesson applied a
4263                // second time. Every `pix_ops` statement is an ELEVATION that
4264                // runs BEFORE the step generates. The performed ARGUMENT here is
4265                // the step's own output, so running it as an elevation would
4266                // hand the handler an unresolved symbol and put a NAME on the
4267                // wire where the adopter expected a token — a defect that shows
4268                // up as garbage output, never as an error.
4269                TokenType::Perform => {
4270                    let p = self.parse_perform_step()?;
4271                    node.performs.push(p);
4272                }
4273                // Sub-construct (probe, non-statement form) → skip structurally.
4274                // The REAL `probe … for […]` statement is taken by the guarded
4275                // arm above; this catches only the bare legacy shape.
4276                TokenType::Probe => {
4277                    self.skip_flow_step_structural()?;
4278                }
4279                _ => {
4280                    return Err(ParseError {
4281                        message: format!(
4282                            "Unexpected token in step body: '{}' — expected given, ask, \
4283                             probe, reason, weave, stream, perform, output, confidence_floor, \
4284                             navigate, apply, requires_context, now",
4285                            inner.value
4286                        ),
4287                        line: inner.line,
4288                        column: inner.column,
4289                                            ..Default::default()
4290                    });
4291                }
4292            }
4293        }
4294        Ok(())
4295    }
4296
4297    /// Skip a flow-level sub-construct structurally (consume keyword + args + optional block).
4298    fn skip_flow_step_structural(&mut self) -> Result<(), ParseError> {
4299        // Consume the keyword
4300        self.advance();
4301        // Consume tokens until we hit a { or a closing }, or a known flow step keyword
4302        while !self.check(TokenType::LBrace)
4303            && !self.check(TokenType::RBrace)
4304            && !self.check(TokenType::Eof)
4305        {
4306            // Check if we hit a new step-level keyword (means this was a one-liner)
4307            let tt = &self.current().ttype;
4308            if matches!(
4309                tt,
4310                TokenType::Step
4311                    | TokenType::Given
4312                    | TokenType::Ask
4313                    | TokenType::Output
4314                    | TokenType::Navigate
4315                    | TokenType::Use
4316                    | TokenType::Probe
4317                    | TokenType::Reason
4318                    | TokenType::Weave
4319                    | TokenType::Stream
4320                    | TokenType::If
4321                    | TokenType::For
4322                    | TokenType::Let
4323                    | TokenType::Return
4324            ) {
4325                return Ok(());
4326            }
4327            self.advance();
4328        }
4329        // If block, skip it
4330        if self.check(TokenType::LBrace) {
4331            self.skip_braced_block()?;
4332        }
4333        Ok(())
4334    }
4335
4336    // ── INTENT ───────────────────────────────────────────────────
4337
4338    fn parse_intent(&mut self) -> Result<IntentNode, ParseError> {
4339        let tok = self.consume(TokenType::Intent)?;
4340        let loc = self.loc_of(&tok);
4341        let name = self.consume(TokenType::Identifier)?.value;
4342        self.consume(TokenType::LBrace)?;
4343
4344        let mut node = IntentNode {
4345            name,
4346            given: String::new(),
4347            ask: String::new(),
4348            output_type: None,
4349            confidence_floor: None,
4350            loc,
4351            leading_trivia: Vec::new(),
4352            trailing_trivia: Vec::new(),
4353        };
4354
4355        while !self.check(TokenType::RBrace) {
4356            let field_name = self.current().value.clone();
4357            self.advance();
4358            self.consume(TokenType::Colon)?;
4359
4360            match field_name.as_str() {
4361                "given" => node.given = self.consume(TokenType::Identifier)?.value,
4362                "ask" => node.ask = self.consume(TokenType::StringLit)?.value,
4363                "output" => node.output_type = Some(self.parse_type_expr()?),
4364                "confidence_floor" => node.confidence_floor = Some(self.consume_number()?),
4365                _ => self.skip_value(),
4366            }
4367        }
4368        self.consume(TokenType::RBrace)?;
4369        Ok(node)
4370    }
4371
4372    // ── RUN ──────────────────────────────────────────────────────
4373
4374    fn parse_run(&mut self) -> Result<RunStatement, ParseError> {
4375        let tok = self.consume(TokenType::Run)?;
4376        let loc = self.loc_of(&tok);
4377        let flow_name = self.consume(TokenType::Identifier)?.value;
4378
4379        self.consume(TokenType::LParen)?;
4380        let mut arguments = Vec::new();
4381        if !self.check(TokenType::RParen) {
4382            arguments = self.parse_argument_list()?;
4383        }
4384        self.consume(TokenType::RParen)?;
4385
4386        let mut node = RunStatement {
4387            flow_name,
4388            arguments,
4389            persona: String::new(),
4390            context: String::new(),
4391            anchors: Vec::new(),
4392            on_failure: String::new(),
4393            on_failure_params: Vec::new(),
4394            output_to: String::new(),
4395            effort: String::new(),
4396            loc,
4397            leading_trivia: Vec::new(),
4398            trailing_trivia: Vec::new(),
4399        };
4400
4401        while self.check_run_modifier() {
4402            let mod_tok = self.current().clone();
4403            // v2.83.0 — `with <Persona>`, README's spelling of `as`.
4404            if mod_tok.value == "with" && mod_tok.ttype != TokenType::As {
4405                self.advance();
4406                node.persona = self.consume(TokenType::Identifier)?.value;
4407                continue;
4408            }
4409            match mod_tok.ttype {
4410                TokenType::As => {
4411                    self.advance();
4412                    node.persona = self.consume(TokenType::Identifier)?.value;
4413                }
4414                TokenType::Within => {
4415                    self.advance();
4416                    node.context = self.consume(TokenType::Identifier)?.value;
4417                }
4418                TokenType::ConstrainedBy => {
4419                    self.advance();
4420                    node.anchors = self.parse_bracketed_identifiers()?;
4421                }
4422                TokenType::OnFailure => {
4423                    self.advance();
4424                    self.consume(TokenType::Colon)?;
4425                    node.on_failure = self.consume_any_ident_or_kw()?.value;
4426                    // Parse optional params: (key: val, ...)
4427                    if self.check(TokenType::LParen) {
4428                        self.advance();
4429                        while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
4430                            let key = self.consume_any_ident_or_kw()?.value;
4431                            self.consume(TokenType::Colon)?;
4432                            let val = self.consume_any_ident_or_kw()?.value;
4433                            node.on_failure_params.push((key, val));
4434                            if self.check(TokenType::Comma) {
4435                                self.advance();
4436                            }
4437                        }
4438                        if self.check(TokenType::RParen) {
4439                            self.advance();
4440                        }
4441                    }
4442                }
4443                TokenType::OutputTo => {
4444                    self.advance();
4445                    self.consume(TokenType::Colon)?;
4446                    node.output_to = self.consume(TokenType::StringLit)?.value;
4447                }
4448                TokenType::Effort => {
4449                    self.advance();
4450                    self.consume(TokenType::Colon)?;
4451                    node.effort = self.consume_any_ident_or_kw()?.value;
4452                }
4453                _ => break,
4454            }
4455        }
4456
4457        Ok(node)
4458    }
4459
4460    // ── EPISTEMIC BLOCK ──────────────────────────────────────────
4461
4462    fn parse_epistemic_block(&mut self) -> Result<EpistemicBlock, ParseError> {
4463        let tok = self.current().clone();
4464        let mode = match tok.ttype {
4465            TokenType::Know => "know",
4466            TokenType::Believe => "believe",
4467            TokenType::Speculate => "speculate",
4468            TokenType::Doubt => "doubt",
4469            _ => unreachable!(),
4470        };
4471        self.advance();
4472        let loc = self.loc_of(&tok);
4473
4474        self.consume(TokenType::LBrace)?;
4475        let mut body = Vec::new();
4476        while !self.check(TokenType::RBrace) {
4477            body.push(self.parse_declaration()?);
4478        }
4479        self.consume(TokenType::RBrace)?;
4480
4481        Ok(EpistemicBlock {
4482            mode: mode.to_string(),
4483            body,
4484            loc,
4485            leading_trivia: Vec::new(),
4486            trailing_trivia: Vec::new(),
4487        })
4488    }
4489
4490    // ── IF ────────────────────────────────────────────────────────
4491
4492    // ── v2.26.0 — the pure expression engine (Pratt parser) ───────────
4493
4494    /// Parse a pure expression (v2.26.0). Precedence-climbing: `or` < `and` <
4495    /// comparison < `+ -` < `* / %` < unary (`- not`) < atom. Total + pure; no
4496    /// side effects. Field/index access + the builtin catalog land in v2.26.0.
4497    fn parse_expr(&mut self) -> Result<Expr, ParseError> {
4498        self.parse_expr_bp(0)
4499    }
4500
4501    fn parse_expr_bp(&mut self, min_bp: u8) -> Result<Expr, ParseError> {
4502        // Prefix: unary `-` (negation) / `not` (boolean). Binds tighter than
4503        // every binary operator (bp 6).
4504        let mut lhs = match self.current().ttype {
4505            TokenType::Minus => {
4506                self.advance();
4507                Expr::Unary(UnOp::Neg, Box::new(self.parse_expr_bp(6)?))
4508            }
4509            TokenType::Not => {
4510                self.advance();
4511                Expr::Unary(UnOp::Not, Box::new(self.parse_expr_bp(6)?))
4512            }
4513            _ => self.parse_postfix()?,
4514        };
4515        // Infix: left-associative (right_bp = left_bp + 1).
4516        while let Some((op, lbp)) = Self::binop_of(self.current().ttype.clone()) {
4517            if lbp < min_bp {
4518                break;
4519            }
4520            self.advance();
4521            let rhs = self.parse_expr_bp(lbp + 1)?;
4522            lhs = Expr::Binary(op, Box::new(lhs), Box::new(rhs));
4523        }
4524        Ok(lhs)
4525    }
4526
4527    /// Map a token to `(BinOp, left binding power)`, or `None` if it is not an
4528    /// infix operator (which stops the climb — e.g. at `->` or `{`).
4529    fn binop_of(t: TokenType) -> Option<(BinOp, u8)> {
4530        Some(match t {
4531            TokenType::Or => (BinOp::Or, 1),
4532            TokenType::And => (BinOp::And, 2),
4533            TokenType::Eq => (BinOp::Eq, 3),
4534            TokenType::Neq => (BinOp::Ne, 3),
4535            TokenType::Lt => (BinOp::Lt, 3),
4536            TokenType::Lte => (BinOp::Le, 3),
4537            TokenType::Gt => (BinOp::Gt, 3),
4538            TokenType::Gte => (BinOp::Ge, 3),
4539            TokenType::Plus => (BinOp::Add, 4),
4540            TokenType::Minus => (BinOp::Sub, 4),
4541            TokenType::Star => (BinOp::Mul, 5),
4542            TokenType::Slash => (BinOp::Div, 5),
4543            TokenType::Percent => (BinOp::Mod, 5),
4544            _ => return None,
4545        })
4546    }
4547
4548    /// v2.26.0 — parse a primary then its `.` postfix chain: a builtin call
4549    /// (`.length`, `.contains(x)`) when the name is in the closed catalog, else
4550    /// a dotted reference-path continuation (`a.b.c` → `Ref("a.b.c")`, the
4551    /// pre-v2.26.0 behaviour). Field access on a non-reference (`(a+b).x`) is
4552    /// reserved for v2.26.0.
4553    fn parse_postfix(&mut self) -> Result<Expr, ParseError> {
4554        let mut expr = self.parse_expr_atom()?;
4555        loop {
4556            if self.check(TokenType::Dot) {
4557                self.advance();
4558                let name = self.consume_any_ident_or_kw()?.value;
4559                if let Some(builtin) = Builtin::from_name(&name) {
4560                    let mut args = vec![expr];
4561                    if self.check(TokenType::LParen) {
4562                        self.advance();
4563                        while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
4564                            args.push(self.parse_expr_bp(0)?);
4565                            if self.check(TokenType::Comma) {
4566                                self.advance();
4567                            } else {
4568                                break;
4569                            }
4570                        }
4571                        self.consume(TokenType::RParen)?;
4572                    }
4573                    expr = Expr::Call(builtin, args);
4574                } else {
4575                    // v2.26.0 — a plain dotted path on a Ref extends the Ref
4576                    // (back-compat: `a.b.c` → `Ref("a.b.c")`); on any other base
4577                    // it is a structured field access (the JSONB seam).
4578                    expr = match expr {
4579                        Expr::Ref(p) => Expr::Ref(format!("{p}.{name}")),
4580                        other => Expr::Field(Box::new(other), name),
4581                    };
4582                }
4583            } else if self.check(TokenType::LBracket) {
4584                // v2.26.0 — index access `base[index]`.
4585                self.advance();
4586                let index = self.parse_expr_bp(0)?;
4587                self.consume(TokenType::RBracket)?;
4588                expr = Expr::Index(Box::new(expr), Box::new(index));
4589            } else {
4590                break;
4591            }
4592        }
4593        Ok(expr)
4594    }
4595
4596    fn parse_expr_atom(&mut self) -> Result<Expr, ParseError> {
4597        let tok = self.current().clone();
4598        match tok.ttype {
4599            TokenType::Integer => {
4600                self.advance();
4601                let lit = tok
4602                    .value
4603                    .parse::<i64>()
4604                    .map(ExprLit::Int)
4605                    .or_else(|_| tok.value.parse::<f64>().map(ExprLit::Float))
4606                    .map_err(|_| ParseError {
4607                        message: format!("invalid integer literal '{}'", tok.value),
4608                        line: tok.line,
4609                        column: tok.column,
4610                        ..Default::default()
4611                    })?;
4612                Ok(Expr::Lit(lit))
4613            }
4614            TokenType::Float => {
4615                self.advance();
4616                let f = tok.value.parse::<f64>().map_err(|_| ParseError {
4617                    message: format!("invalid float literal '{}'", tok.value),
4618                    line: tok.line,
4619                    column: tok.column,
4620                    ..Default::default()
4621                })?;
4622                Ok(Expr::Lit(ExprLit::Float(f)))
4623            }
4624            TokenType::Bool => {
4625                self.advance();
4626                Ok(Expr::Lit(ExprLit::Bool(tok.value == "true")))
4627            }
4628            TokenType::StringLit => {
4629                self.advance();
4630                Ok(Expr::Lit(ExprLit::Str(tok.value)))
4631            }
4632            TokenType::LParen => {
4633                self.advance();
4634                let inner = self.parse_expr_bp(0)?;
4635                self.consume(TokenType::RParen)?;
4636                Ok(inner)
4637            }
4638            _ => {
4639                // Reference: a single identifier (or keyword used as a name).
4640                // The `.` chain (dotted path / builtin call) is handled by the
4641                // postfix layer (v2.26.0 `parse_postfix`).
4642                Ok(Expr::Ref(self.consume_any_ident_or_kw()?.value))
4643            }
4644        }
4645    }
4646
4647    /// v2.26.0 — render a literal to its legacy surface string (for the
4648    /// back-compat `(condition, op, value)` triple). Only used when an
4649    /// expression fits the legacy shape; numeric round-tripping is exact for
4650    /// ints and faithful-enough for floats (the legacy runtime re-parses it).
4651    fn expr_lit_surface(lit: &ExprLit) -> String {
4652        match lit {
4653            ExprLit::Int(i) => i.to_string(),
4654            ExprLit::Float(f) => f.to_string(),
4655            ExprLit::Bool(b) => b.to_string(),
4656            ExprLit::Str(s) => s.clone(),
4657        }
4658    }
4659
4660    fn expr_leaf_surface(expr: &Expr) -> Option<String> {
4661        match expr {
4662            Expr::Ref(p) => Some(p.clone()),
4663            Expr::Lit(l) => Some(Self::expr_lit_surface(l)),
4664            _ => None,
4665        }
4666    }
4667
4668    /// A legacy "leaf" is a bare reference (truthy check) or a
4669    /// `<ref> <cmp> <ref|literal>` triple — exactly what the pre-v2.26.0 `if`
4670    /// grammar could express.
4671    fn expr_legacy_leaf(expr: &Expr) -> Option<(String, String, String)> {
4672        match expr {
4673            Expr::Ref(p) => Some((p.clone(), String::new(), String::new())),
4674            Expr::Binary(op, l, r) => {
4675                let op_s = match op {
4676                    BinOp::Eq => "==",
4677                    BinOp::Ne => "!=",
4678                    BinOp::Lt => "<",
4679                    BinOp::Le => "<=",
4680                    BinOp::Gt => ">",
4681                    BinOp::Ge => ">=",
4682                    _ => return None,
4683                };
4684                let lhs = match &**l {
4685                    Expr::Ref(p) => p.clone(),
4686                    _ => return None,
4687                };
4688                let rhs = Self::expr_leaf_surface(r)?;
4689                Some((lhs, op_s.to_string(), rhs))
4690            }
4691            _ => None,
4692        }
4693    }
4694
4695    /// Flatten an `or`-tree of legacy leaves in left-to-right order. Returns
4696    /// `false` (and leaves `out` unusable) if any node is not a legacy leaf.
4697    fn collect_or_leaves(expr: &Expr, out: &mut Vec<(String, String, String)>) -> bool {
4698        match expr {
4699            Expr::Binary(BinOp::Or, l, r) => {
4700                Self::collect_or_leaves(l, out) && Self::collect_or_leaves(r, out)
4701            }
4702            _ => match Self::expr_legacy_leaf(expr) {
4703                Some(t) => {
4704                    out.push(t);
4705                    true
4706                }
4707                None => false,
4708            },
4709        }
4710    }
4711
4712    /// v2.26.0 — if the parsed condition fits the legacy
4713    /// `(condition, op, value)` + `or`-chain shape, return the legacy fields so
4714    /// the IR + runtime stay byte-identical to pre-v2.26.0 (zero drift). `None` ⇒
4715    /// the condition uses richer forms (`and`, `not`, arithmetic, parentheses,
4716    /// nesting) and must ride the `cond` expression evaluator.
4717    #[allow(clippy::type_complexity)]
4718    fn cond_as_legacy(
4719        expr: &Expr,
4720    ) -> Option<(String, String, String, Vec<(String, String, String)>, String)> {
4721        let mut leaves = Vec::new();
4722        if !Self::collect_or_leaves(expr, &mut leaves) || leaves.is_empty() {
4723            return None;
4724        }
4725        let (c0, o0, v0) = leaves[0].clone();
4726        let rest = leaves[1..].to_vec();
4727        let conjunctor = if rest.is_empty() {
4728            String::new()
4729        } else {
4730            "or".to_string()
4731        };
4732        Some((c0, o0, v0, rest, conjunctor))
4733    }
4734
4735    fn parse_if(&mut self) -> Result<ConditionalNode, ParseError> {
4736        let tok = self.consume(TokenType::If)?;
4737        let loc = self.loc_of(&tok);
4738
4739        // v2.26.0 — parse the condition as a pure expression, then split:
4740        // a legacy-expressible condition populates the legacy triple fields
4741        // (cond = None → byte-identical IR + eval); a richer condition rides
4742        // the `cond` expression evaluator.
4743        let expr = self.parse_expr()?;
4744        let (condition, comparison_op, comparison_value, conditions, conjunctor, cond) =
4745            match Self::cond_as_legacy(&expr) {
4746                Some((c, o, v, more, conj)) => (c, o, v, more, conj, None),
4747                None => (
4748                    String::new(),
4749                    String::new(),
4750                    String::new(),
4751                    Vec::new(),
4752                    String::new(),
4753                    Some(expr),
4754                ),
4755            };
4756
4757        let mut then_body = Vec::new();
4758        let mut else_body = Vec::new();
4759
4760        // Arrow form or block form
4761        if self.check(TokenType::Arrow) {
4762            self.advance();
4763            then_body.push(self.parse_flow_step()?);
4764        } else if self.check(TokenType::LBrace) {
4765            self.advance();
4766            while !self.check(TokenType::RBrace) {
4767                then_body.push(self.parse_flow_step()?);
4768            }
4769            self.consume(TokenType::RBrace)?;
4770        }
4771
4772        // Else branch
4773        if self.check(TokenType::Else) {
4774            self.advance();
4775            if self.check(TokenType::Arrow) {
4776                self.advance();
4777                else_body.push(self.parse_flow_step()?);
4778            } else if self.check(TokenType::LBrace) {
4779                self.advance();
4780                while !self.check(TokenType::RBrace) {
4781                    else_body.push(self.parse_flow_step()?);
4782                }
4783                self.consume(TokenType::RBrace)?;
4784            }
4785        }
4786
4787        Ok(ConditionalNode {
4788            condition,
4789            comparison_op,
4790            comparison_value,
4791            then_body,
4792            else_body,
4793            conditions,
4794            conjunctor,
4795            cond,
4796            loc,
4797        })
4798    }
4799
4800    // ── FOR IN ───────────────────────────────────────────────────
4801
4802    fn parse_for_in(&mut self) -> Result<ForInStatement, ParseError> {
4803        let tok = self.consume(TokenType::For)?;
4804        let loc = self.loc_of(&tok);
4805        let variable = self.consume(TokenType::Identifier)?.value;
4806        self.consume(TokenType::In)?;
4807        let iterable = self.parse_dotted_identifier()?;
4808
4809        self.consume(TokenType::LBrace)?;
4810        // v1.14.0 — increment loop_depth so `parse_break` /
4811        // `parse_continue` inside the body pass the scope check.
4812        // Decrement on every exit path (Ok / Err) so a parse error
4813        // mid-body does not leave the depth permanently elevated
4814        // for later top-level parsing — `?` would skip the
4815        // decrement otherwise.
4816        self.loop_depth += 1;
4817        let body_result = (|| -> Result<Vec<FlowStep>, ParseError> {
4818            let mut body = Vec::new();
4819            while !self.check(TokenType::RBrace) {
4820                body.push(self.parse_flow_step()?);
4821            }
4822            Ok(body)
4823        })();
4824        self.loop_depth -= 1;
4825        let body = body_result?;
4826        self.consume(TokenType::RBrace)?;
4827
4828        Ok(ForInStatement {
4829            variable,
4830            iterable,
4831            body,
4832            loc,
4833        })
4834    }
4835
4836    /// v1.14.0 — `break` keyword. Compile-time scope check
4837    /// (`loop_depth == 0`) rejects break outside a for-in body.
4838    fn parse_break(&mut self) -> Result<BreakStatement, ParseError> {
4839        let tok = self.consume(TokenType::Break)?;
4840        let loc = self.loc_of(&tok);
4841        if self.loop_depth == 0 {
4842            return Err(ParseError {
4843                message: "'break' outside of a for-in loop body".to_string(),
4844                line: tok.line,
4845                column: tok.column,
4846                            ..Default::default()
4847            });
4848        }
4849        Ok(BreakStatement { loc })
4850    }
4851
4852    /// v1.14.0 — `continue` keyword. Same scope check as
4853    /// `parse_break`.
4854    fn parse_continue(&mut self) -> Result<ContinueStatement, ParseError> {
4855        let tok = self.consume(TokenType::Continue)?;
4856        let loc = self.loc_of(&tok);
4857        if self.loop_depth == 0 {
4858            return Err(ParseError {
4859                message: "'continue' outside of a for-in loop body".to_string(),
4860                line: tok.line,
4861                column: tok.column,
4862                            ..Default::default()
4863            });
4864        }
4865        Ok(ContinueStatement { loc })
4866    }
4867
4868    // ── LET ──────────────────────────────────────────────────────
4869
4870    fn parse_let(&mut self) -> Result<LetStatement, ParseError> {
4871        let tok = self.consume(TokenType::Let)?;
4872        let loc = self.loc_of(&tok);
4873
4874        // Name can be an identifier or a keyword used as binding name
4875        let name = self.consume_any_ident_or_kw()?.value;
4876        // v2.4.0 — optional type annotation `let x: <TypeExpr> = …`.
4877        let type_annotation = if self.check(TokenType::Colon) {
4878            self.advance();
4879            Some(self.parse_type_expr()?)
4880        } else {
4881            None
4882        };
4883        self.consume(TokenType::Assign)?;
4884        // v1.12.0 — reset side-channel before parsing value; the
4885        // atom / expr helpers tag the kind as they descend.
4886        self.last_let_value_kind = "literal".to_string();
4887        let (value, value_ast) = self.parse_let_value_expr_with_ast()?;
4888
4889        Ok(LetStatement {
4890            identifier: name,
4891            value_expr: value,
4892            value_kind: self.last_let_value_kind.clone(),
4893            type_annotation,
4894            value_ast,
4895            loc,
4896            leading_trivia: Vec::new(),
4897            trailing_trivia: Vec::new(),
4898        })
4899    }
4900
4901    fn parse_let_value_expr(&mut self) -> Result<String, ParseError> {
4902        let atom = self.parse_let_atom()?;
4903
4904        // Arithmetic expression: collect as string
4905        if matches!(
4906            self.current().ttype,
4907            TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
4908        ) {
4909            let mut parts = vec![atom];
4910            while matches!(
4911                self.current().ttype,
4912                TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
4913            ) {
4914                parts.push(self.advance().value.clone());
4915                parts.push(self.parse_let_atom()?);
4916            }
4917            self.last_let_value_kind = "expression".to_string();
4918            return Ok(parts.join(" "));
4919        }
4920        Ok(atom)
4921    }
4922
4923    /// v2.26.0 — parse a `let`-binding value, additionally producing a
4924    /// structured `value_ast` for the expression case. A list literal keeps the
4925    /// dedicated path; everything else is parsed through the v2.26.0 expression
4926    /// engine and classified: a bare literal / reference keeps its pre-v2.26.0
4927    /// string form (`value_ast = None`, byte-identical), while a real expression
4928    /// (`price * qty`, `recent.length`) additionally carries a `value_ast` the
4929    /// runtime evaluates for real (pre-v2.26.0 it was treated as an opaque literal
4930    /// string). Used ONLY by `parse_let` — other value positions (list items,
4931    /// remember/stream values) keep the string-only `parse_let_value_expr`.
4932    fn parse_let_value_expr_with_ast(&mut self) -> Result<(String, Option<Expr>), ParseError> {
4933        if self.check(TokenType::LBracket) {
4934            self.last_let_value_kind = "literal".to_string();
4935            return Ok((self.parse_let_list_literal()?, None));
4936        }
4937        let expr = self.parse_expr()?;
4938        Ok(match expr {
4939            Expr::Lit(lit) => {
4940                self.last_let_value_kind = "literal".to_string();
4941                (Self::expr_lit_surface(&lit), None)
4942            }
4943            Expr::Ref(p) => {
4944                self.last_let_value_kind = "reference".to_string();
4945                (p, None)
4946            }
4947            other => {
4948                self.last_let_value_kind = "expression".to_string();
4949                (Self::render_expr(&other), Some(other))
4950            }
4951        })
4952    }
4953
4954    /// v2.26.0 — a readable surface rendering of an expression for the
4955    /// vestigial `value_expr` string (the runtime uses `value_ast`).
4956    fn render_expr(e: &Expr) -> String {
4957        match e {
4958            Expr::Lit(l) => Self::expr_lit_surface(l),
4959            Expr::Ref(p) => p.clone(),
4960            // v2.83.0 — surface form of a `logic { }` chain. This string is
4961            // vestigial (the runtime evaluates `value_ast`), so it renders the
4962            // shape rather than trying to reconstruct the author's layout.
4963            Expr::Let { name, value, body } => format!(
4964                "let {name} = {} in {}",
4965                Self::render_expr(value),
4966                Self::render_expr(body)
4967            ),
4968            Expr::Unary(UnOp::Neg, x) => format!("-{}", Self::render_expr(x)),
4969            Expr::Unary(UnOp::Not, x) => format!("not {}", Self::render_expr(x)),
4970            Expr::Binary(op, l, r) => {
4971                let sym = match op {
4972                    BinOp::Add => "+",
4973                    BinOp::Sub => "-",
4974                    BinOp::Mul => "*",
4975                    BinOp::Div => "/",
4976                    BinOp::Mod => "%",
4977                    BinOp::Eq => "==",
4978                    BinOp::Ne => "!=",
4979                    BinOp::Lt => "<",
4980                    BinOp::Le => "<=",
4981                    BinOp::Gt => ">",
4982                    BinOp::Ge => ">=",
4983                    BinOp::And => "and",
4984                    BinOp::Or => "or",
4985                };
4986                format!("({} {sym} {})", Self::render_expr(l), Self::render_expr(r))
4987            }
4988            Expr::Call(b, args) => {
4989                let recv = args.first().map(Self::render_expr).unwrap_or_default();
4990                let rest: Vec<String> = args.iter().skip(1).map(Self::render_expr).collect();
4991                if rest.is_empty() {
4992                    format!("{recv}.{}", b.surface())
4993                } else {
4994                    format!("{recv}.{}({})", b.surface(), rest.join(", "))
4995                }
4996            }
4997            Expr::Field(b, f) => format!("{}.{f}", Self::render_expr(b)),
4998            Expr::Index(b, i) => format!("{}[{}]", Self::render_expr(b), Self::render_expr(i)),
4999        }
5000    }
5001
5002    fn parse_let_atom(&mut self) -> Result<String, ParseError> {
5003        let tok = self.current().clone();
5004
5005        match tok.ttype {
5006            TokenType::StringLit => {
5007                self.last_let_value_kind = "literal".to_string();
5008                self.advance();
5009                Ok(tok.value)
5010            }
5011            TokenType::Integer | TokenType::Float => {
5012                self.last_let_value_kind = "literal".to_string();
5013                self.advance();
5014                Ok(tok.value)
5015            }
5016            TokenType::Bool => {
5017                self.last_let_value_kind = "literal".to_string();
5018                self.advance();
5019                Ok(tok.value)
5020            }
5021            TokenType::Identifier => {
5022                self.last_let_value_kind = "reference".to_string();
5023                self.parse_dotted_identifier()
5024            }
5025            TokenType::LBracket => {
5026                self.last_let_value_kind = "literal".to_string();
5027                self.parse_let_list_literal()
5028            }
5029            _ => {
5030                // Keywords starting a dotted path (pix.document_tree)
5031                if self.pos + 1 < self.tokens.len()
5032                    && self.tokens[self.pos + 1].ttype == TokenType::Dot
5033                {
5034                    self.last_let_value_kind = "reference".to_string();
5035                    return self.parse_dotted_identifier();
5036                }
5037                Err(ParseError {
5038                    message: format!(
5039                        "Expected value expression, found {:?}('{}')",
5040                        tok.ttype, tok.value
5041                    ),
5042                    line: tok.line,
5043                    column: tok.column,
5044                                    ..Default::default()
5045                })
5046            }
5047        }
5048    }
5049
5050    fn parse_let_list_literal(&mut self) -> Result<String, ParseError> {
5051        self.consume(TokenType::LBracket)?;
5052        let mut items = Vec::new();
5053        if !self.check(TokenType::RBracket) {
5054            items.push(self.parse_let_value_expr()?);
5055            while self.check(TokenType::Comma) {
5056                self.advance();
5057                if self.check(TokenType::RBracket) {
5058                    break; // trailing comma
5059                }
5060                items.push(self.parse_let_value_expr()?);
5061            }
5062        }
5063        self.consume(TokenType::RBracket)?;
5064        Ok(format!("[{}]", items.join(", ")))
5065    }
5066
5067    // ── RETURN ───────────────────────────────────────────────────
5068
5069    fn parse_return(&mut self) -> Result<ReturnStatement, ParseError> {
5070        let tok = self.consume(TokenType::Return)?;
5071        let loc = self.loc_of(&tok);
5072        let value = self.parse_let_value_expr()?;
5073        Ok(ReturnStatement {
5074            value_expr: value,
5075            loc,
5076        })
5077    }
5078
5079    // ── TIER 2 FLOW STEP HELPERS ────────────────────────────────────
5080
5081    /// Parse: keyword target (consumes keyword + one identifier/keyword-as-value).
5082    fn parse_flow_step_simple(&mut self, _kw: &str) -> Result<(Loc, String), ParseError> {
5083        let tok = self.current().clone();
5084        self.advance(); // consume keyword
5085        let target = if self.at_declaration_start()
5086            || self.check(TokenType::RBrace)
5087            || self.check(TokenType::Eof)
5088        {
5089            String::new()
5090        } else {
5091            self.consume_any_ident_or_kw()?.value.clone()
5092        };
5093        // Skip optional braced block
5094        if self.check(TokenType::LBrace) {
5095            self.skip_braced_block()?;
5096        }
5097        Ok((
5098            Loc {
5099                line: tok.line,
5100                column: tok.column,
5101            },
5102            target,
5103        ))
5104    }
5105
5106    /// Parse: keyword { ... } — block-level step, skip body structurally.
5107    /// v2.67.0 — `stream { <steps> }` with a REAL body.
5108    ///
5109    /// The four block primitives (`deliberate`, `consensus`, `stream`,
5110    /// `transact`) all went through [`Self::parse_block_step`], whose entire job
5111    /// is `skip_braced_block()`. Their bodies were discarded at parse time — so
5112    /// their handlers were not no-ops through neglect, they were no-ops
5113    /// *by construction*: there was nothing in the AST to execute. v2.67.0 retracted
5114    /// `transact`; this gives `stream` its body back. `deliberate` / `consensus`
5115    /// remain body-less pending their Tier-4 disposition.
5116    fn parse_stream_block(&mut self) -> Result<StreamBlock, ParseError> {
5117        let tok = self.current().clone();
5118        let loc = self.loc_of(&tok);
5119        self.advance(); // consume `stream`
5120
5121        // v2.83.0 — `<T>`: the CHUNK type, and the reason this is not just a
5122        // cosmetic capture. The skip loop below used to eat it: `stream<QuoteData>`
5123        // advanced straight past `<QuoteData>` looking for `{`, so the one piece of
5124        // type information the author wrote about the stream was discarded before
5125        // anything could check it.
5126        let mut chunk_type = String::new();
5127        if self.check(TokenType::Lt) {
5128            self.advance();
5129            let inner = self.parse_type_expr()?;
5130            chunk_type = if inner.generic_param.is_empty() {
5131                inner.name
5132            } else {
5133                format!("{}<{}>", inner.name, inner.generic_param)
5134            };
5135            self.consume(TokenType::Gt)?;
5136        }
5137
5138        // Tolerate the pre-111 form `stream <effect-ish tokens> { … }`: skip any
5139        // argument tokens ahead of the brace, exactly as `parse_block_step` did,
5140        // so an existing program keeps parsing. Only the BODY changes.
5141        while !self.check(TokenType::LBrace)
5142            && !self.check(TokenType::RBrace)
5143            && !self.check(TokenType::Eof)
5144            && !self.at_declaration_start()
5145        {
5146            self.advance();
5147        }
5148
5149        let mut block = StreamBlock {
5150            chunk_type,
5151            on_chunk: None,
5152            on_complete: None,
5153            on_error: None,
5154            body: Vec::new(),
5155            loc,
5156        };
5157
5158        if self.check(TokenType::LBrace) {
5159            self.advance();
5160            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5161                // v2.83.0 — the two SPECIFIED handler arms. `the design plan`'s D8
5162                // promises `stream<τ> { on_chunk: … on_complete: … }` compiles
5163                // with "cero cambios en `.axon` source files de adopters"; before
5164                // this landed it was a hard parse error at flow level and a
5165                // silent discard in a step body.
5166                let name = self.current().value.clone();
5167                let is_arm = matches!(name.as_str(), "on_chunk" | "on_complete" | "on_error")
5168                    && self
5169                        .tokens
5170                        .get(self.pos + 1)
5171                        .is_some_and(|t| t.ttype == TokenType::Colon);
5172                if is_arm {
5173                    let arm_tok = self.current().clone();
5174                    self.advance(); // the handler name
5175                    self.advance(); // `:`
5176                    let arm = self.parse_stream_handler_arm(&name, &arm_tok)?;
5177                    let slot = match name.as_str() {
5178                        "on_chunk" => &mut block.on_chunk,
5179                        "on_complete" => &mut block.on_complete,
5180                        _ => &mut block.on_error,
5181                    };
5182                    if slot.is_some() {
5183                        return Err(ParseError {
5184                            message: format!(
5185                                "`{name}` is declared twice in this `stream` block. Two handlers \
5186                                 for one edge have no defined composition (whose output is the \
5187                                 stream's?), so the duplicate is refused rather than silently \
5188                                 overwriting the first."
5189                            ),
5190                            line: arm_tok.line,
5191                            column: arm_tok.column,
5192                            ..Default::default()
5193                        });
5194                    }
5195                    *slot = Some(arm);
5196                    continue;
5197                }
5198
5199                // A `<ident>: {` that is NOT one of the two arms is a TYPO in a
5200                // closed catalog, and the v2.83.0 discipline says to ask which
5201                // direction the silence fails in: a mis-spelled `on_chunk` would
5202                // fall through to `parse_flow_step` and be reported against the
5203                // brace, pointing the author at the wrong token entirely. Name
5204                // the key and the catalog instead.
5205                let next_two_are_block = self
5206                    .tokens
5207                    .get(self.pos + 1)
5208                    .is_some_and(|t| t.ttype == TokenType::Colon)
5209                    && self
5210                        .tokens
5211                        .get(self.pos + 2)
5212                        .is_some_and(|t| t.ttype == TokenType::LBrace);
5213                if next_two_are_block {
5214                    let bad = self.current().clone();
5215                    return Err(ParseError {
5216                        message: format!(
5217                            "unknown `stream` handler `{name}` — this block accepts only \
5218                             `on_chunk:` (run once per chunk, with the chunk bound as `chunk`), \
5219                             `on_complete:` (run once, after the source closes, with the \
5220                             accumulation bound as `complete`) and `on_error:` (run when the \
5221                             SOURCE fails, with the failure bound as `error`). An unrecognised \
5222                             handler is refused rather than skipped: a skipped handler removes \
5223                             the processing the author wrote, and silence in that direction is \
5224                             indistinguishable from a stream that had nothing to do."
5225                        ),
5226                        line: bad.line,
5227                        column: bad.column,
5228                        ..Default::default()
5229                    });
5230                }
5231
5232                // v2.67.0's body form, kept: `stream { <flow steps> }`.
5233                block.body.push(self.parse_flow_step()?);
5234            }
5235            self.consume(TokenType::RBrace)?;
5236        }
5237
5238        Ok(block)
5239    }
5240
5241    /// v2.83.0 — one `on_chunk:` / `on_complete:` arm, parsed as a STEP body.
5242    ///
5243    /// The arm carries `output:` (README block 15 writes `output: QuoteSnapshot`
5244    /// in `on_chunk` and `output: VerifiedQuote` in `on_complete`), and `output:`
5245    /// is a step field with no flow-level position. Reusing
5246    /// [`Self::parse_step_body_into`] is therefore not a convenience — it is the
5247    /// only shape that accepts what the README publishes, and it means the arm
5248    /// dispatches through `run_step` like any other step.
5249    fn parse_stream_handler_arm(
5250        &mut self,
5251        name: &str,
5252        at: &Token,
5253    ) -> Result<StepNode, ParseError> {
5254        self.consume(TokenType::LBrace)?;
5255        let mut node = StepNode {
5256            name: name.to_string(),
5257            persona_ref: String::new(),
5258            given: String::new(),
5259            ask: String::new(),
5260            output_type: String::new(),
5261            confidence_floor: None,
5262            navigate_ref: String::new(),
5263            apply_ref: String::new(),
5264            requires_context: None,
5265            now_tz: None,
5266            guards: Vec::new(),
5267            pix_ops: Vec::new(),
5268            stream: None,
5269            performs: Vec::new(),
5270            loc: self.loc_of(at),
5271        };
5272        self.parse_step_body_into(&mut node)?;
5273        self.consume(TokenType::RBrace)?;
5274        Ok(node)
5275    }
5276
5277    fn parse_block_step(&mut self, _kw: &str) -> Result<Loc, ParseError> {
5278        let tok = self.current().clone();
5279        self.advance();
5280        // Skip optional arguments before brace
5281        while !self.check(TokenType::LBrace)
5282            && !self.check(TokenType::RBrace)
5283            && !self.check(TokenType::Eof)
5284            && !self.at_declaration_start()
5285        {
5286            self.advance();
5287        }
5288        if self.check(TokenType::LBrace) {
5289            self.skip_braced_block()?;
5290        }
5291        Ok(Loc {
5292            line: tok.line,
5293            column: tok.column,
5294        })
5295    }
5296
5297    /// v2.41.0 — parse `forge <Name>(seed: "<text>") -> <Type> { mode:,
5298    /// novelty:, depth:, branches:, constraints: }`. Real field capture
5299    /// (replacing the pre-v2.41.0 discard-everything stub). Strict closed-catalog:
5300    /// an unknown field is a hard parse error; all cross-field laws (Boden mode
5301    /// catalog, novelty range, depth/branches ≥ 1, `constraints:` → `anchor`)
5302    /// are v2.41.0 type-checker territory.
5303    fn parse_forge_step(&mut self) -> Result<ForgeBlock, ParseError> {
5304        let tok = self.consume(TokenType::Forge)?;
5305        let name = self.consume(TokenType::Identifier)?.value;
5306        let mut node = ForgeBlock {
5307            name,
5308            novelty: 0.5,
5309            depth: 1,
5310            branches: 1,
5311            loc: Loc { line: tok.line, column: tok.column },
5312            ..Default::default()
5313        };
5314        // `(seed: "...")`
5315        self.consume(TokenType::LParen)?;
5316        let arg = self.consume_any_ident_or_kw()?.value;
5317        self.consume(TokenType::Colon)?;
5318        if arg != "seed" {
5319            return Err(self.error(&format!(
5320                "forge '{}' expects `seed:` as its argument, found `{arg}`",
5321                node.name
5322            )));
5323        }
5324        node.seed = self.consume(TokenType::StringLit)?.value;
5325        self.consume(TokenType::RParen)?;
5326        // `-> <Type>`
5327        self.consume(TokenType::Arrow)?;
5328        node.output_type = self.consume_any_ident_or_kw()?.value;
5329        // `{ fields }`
5330        self.consume(TokenType::LBrace)?;
5331        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5332            let field = self.consume_any_ident_or_kw()?.value;
5333            self.consume(TokenType::Colon)?;
5334            match field.as_str() {
5335                "mode" => node.mode = self.consume_any_ident_or_kw()?.value,
5336                "novelty" => node.novelty = self.consume_number()?,
5337                "depth" => {
5338                    node.depth = self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0)
5339                }
5340                "branches" => {
5341                    node.branches =
5342                        self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0)
5343                }
5344                "constraints" => node.constraints_ref = self.consume_any_ident_or_kw()?.value,
5345                other => {
5346                    return Err(self.error(&format!("unknown forge field `{other}`")))
5347                }
5348            }
5349            if self.check(TokenType::Comma) {
5350                self.consume(TokenType::Comma)?;
5351            }
5352        }
5353        self.consume(TokenType::RBrace)?;
5354        Ok(node)
5355    }
5356
5357    /// v2.15.0 — Parse `par { stmt1 stmt2 … }` into CONCURRENT branches.
5358    /// Each top-level flow statement inside the block is one branch (a
5359    /// single-statement body); they execute concurrently at runtime
5360    /// (`flow_dispatcher::parallel::run_branches_concurrently`). Before v2.15.0 the
5361    /// `par` body was skipped (`parse_block_step`), so the branches were lost
5362    /// and the handler ran as a stub. Multi-statement branches (grouping
5363    /// several steps into one sequential branch) are a future grammar
5364    /// extension; today the natural `par { step A  step B }` fans A and B out.
5365    fn parse_par_block(&mut self) -> Result<ParBlock, ParseError> {
5366        let tok = self.current().clone();
5367        self.advance(); // consume `par`
5368        self.consume(TokenType::LBrace)?;
5369        let mut branches: Vec<Vec<FlowStep>> = Vec::new();
5370        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5371            branches.push(vec![self.parse_flow_step()?]);
5372        }
5373        self.consume(TokenType::RBrace)?;
5374        Ok(ParBlock {
5375            branches,
5376            loc: Loc {
5377                line: tok.line,
5378                column: tok.column,
5379            },
5380        })
5381    }
5382
5383    /// v2.4.0 — Parse the `quant` cognitive block surface.
5384    ///
5385    /// Grammar (the attribute header is OPTIONAL):
5386    /// ```text
5387    /// quant { <flow steps> }
5388    /// quant(encoding: amplitude, observable: M, qubits: 10,
5389    ///       depth: 4, bandwidth: 0.5, reupload: 3, backend: quant_sim) { <flow steps> }
5390    /// ```
5391    /// The bare form (the paper's example) leaves every attribute defaulted
5392    /// (`encoding = amplitude`, `effect = quant_sim`). The body is parsed into
5393    /// real nested `FlowStep`s — like `par` branches — so v2.4.0's Continuous
5394    /// Type Invariant scans actual AST rather than skipped tokens.
5395    /// v2.43.0 — parse `warden(<target>) within <Scope> { <body> }`. The
5396    /// `within <Scope>` clause is MANDATORY at the grammar level (fail-closed by
5397    /// construction: a scopeless warden cannot be written); v2.43.0 checks the
5398    /// scope RESOLVES + the target is in its allowlist.
5399    fn parse_warden(&mut self) -> Result<WardenBlock, ParseError> {
5400        let tok = self.consume(TokenType::Warden)?;
5401        // `(<target>)` — the resource under analysis.
5402        self.consume(TokenType::LParen)?;
5403        let target = self.consume_any_ident_or_kw()?.value;
5404        self.consume(TokenType::RParen)?;
5405        // `within <Scope>` — MANDATORY. Omitting it is a hard parse error.
5406        self.consume(TokenType::Within)?;
5407        let scope_ref = self.consume(TokenType::Identifier)?.value;
5408        let mut block = WardenBlock {
5409            target,
5410            scope_ref,
5411            body: Vec::new(),
5412            loc: Loc {
5413                line: tok.line,
5414                column: tok.column,
5415            },
5416        };
5417        // Body: real nested flow steps (like `quant`/`par`).
5418        self.consume(TokenType::LBrace)?;
5419        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5420            block.body.push(self.parse_flow_step()?);
5421        }
5422        self.consume(TokenType::RBrace)?;
5423        Ok(block)
5424    }
5425
5426    /// v2.43.0 — parse `scope <Name> { targets: [ … ], depth: <ident>,
5427    /// approver: [requires] "<cap>" }`. Flat key:value block (the `cache` shape).
5428    /// Catalog + non-empty validation is v2.43.0. Unknown fields are a hard error
5429    ///: a scope governs an offensive-capable analysis.
5430    fn parse_scope(&mut self) -> Result<ScopeDefinition, ParseError> {
5431        let tok = self.consume(TokenType::Scope)?;
5432        let name = self.consume(TokenType::Identifier)?.value;
5433        let mut node = ScopeDefinition {
5434            name,
5435            loc: Loc {
5436                line: tok.line,
5437                column: tok.column,
5438            },
5439            ..Default::default()
5440        };
5441        self.consume(TokenType::LBrace)?;
5442        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5443            let key = self.consume_any_ident_or_kw()?.value;
5444            self.consume(TokenType::Colon)?;
5445            match key.as_str() {
5446                "targets" => {
5447                    self.consume(TokenType::LBracket)?;
5448                    while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
5449                        let t = if self.check(TokenType::StringLit) {
5450                            self.consume(TokenType::StringLit)?.value
5451                        } else {
5452                            self.consume_any_ident_or_kw()?.value
5453                        };
5454                        node.targets.push(t);
5455                        if self.check(TokenType::Comma) {
5456                            self.advance();
5457                        }
5458                    }
5459                    self.consume(TokenType::RBracket)?;
5460                }
5461                "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
5462                "approver" => {
5463                    // Optional `requires` sugar before the capability string.
5464                    if self.current().value == "requires" {
5465                        self.advance();
5466                    }
5467                    node.approver = self.consume(TokenType::StringLit)?.value;
5468                }
5469                other => {
5470                    return Err(self.error(&format!(
5471                        "unknown scope field `{other}` in scope `{}` — expected \
5472                         `targets` / `depth` / `approver`",
5473                        node.name
5474                    )))
5475                }
5476            }
5477            if self.check(TokenType::Comma) {
5478                self.consume(TokenType::Comma)?;
5479            }
5480        }
5481        self.consume(TokenType::RBrace)?;
5482        Ok(node)
5483    }
5484
5485    fn parse_quant(&mut self) -> Result<QuantBlock, ParseError> {
5486        let tok = self.current().clone();
5487        self.advance(); // consume `quant`
5488
5489        let mut block = QuantBlock {
5490            encoding: None,
5491            observable: None,
5492            qubits: None,
5493            depth: None,
5494            bandwidth: None,
5495            reupload: None,
5496            // D1/D9 default backend: the CPU simulator effect. `qpu_native` is
5497            // opt-in via `backend: qpu_native`.
5498            effect: "quant_sim".to_string(),
5499            body: Vec::new(),
5500            loc: Loc {
5501                line: tok.line,
5502                column: tok.column,
5503            },
5504        };
5505
5506        // ── Optional attribute header: `(key: value, …)` ──
5507        if self.check(TokenType::LParen) {
5508            self.advance();
5509            while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
5510                let key = self.consume_any_ident_or_kw()?.value;
5511                self.consume(TokenType::Colon)?;
5512                match key.as_str() {
5513                    "encoding" => {
5514                        block.encoding = Some(self.consume_any_ident_or_kw()?.value)
5515                    }
5516                    "observable" => {
5517                        block.observable = Some(self.parse_dotted_identifier()?)
5518                    }
5519                    "qubits" => block.qubits = Some(self.consume_number()? as i64),
5520                    "depth" => block.depth = Some(self.consume_number()? as i64),
5521                    "bandwidth" => block.bandwidth = Some(self.consume_number()?),
5522                    // v2.23.0 — data re-uploading layers.
5523                    "reupload" => block.reupload = Some(self.consume_number()? as i64),
5524                    // `backend:` selects the algebraic-effect tag (D1/D9).
5525                    "backend" => block.effect = self.consume_any_ident_or_kw()?.value,
5526                    other => {
5527                        return Err(ParseError {
5528                            message: format!(
5529                                "Unknown `quant` attribute `{other}` — expected one of \
5530                                 encoding, observable, qubits, depth, bandwidth, reupload, backend"
5531                            ),
5532                            line: self.current().line,
5533                            column: self.current().column,
5534                            ..Default::default()
5535                        });
5536                    }
5537                }
5538                // Optional comma between attributes (order-free, trailing-comma ok).
5539                if self.check(TokenType::Comma) {
5540                    self.advance();
5541                }
5542            }
5543            self.consume(TokenType::RParen)?;
5544        }
5545
5546        // ── Body: real nested flow steps (like `par`) ──
5547        self.consume(TokenType::LBrace)?;
5548        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5549            block.body.push(self.parse_flow_step()?);
5550        }
5551        self.consume(TokenType::RBrace)?;
5552
5553        Ok(block)
5554    }
5555
5556    /// v2.4.0 — Parse the `yield <expr>` measurement point. Reuses the
5557    /// `let`-value expression grammar (reference / literal / arithmetic) so the
5558    /// yielded value's tokenization intent is preserved in `value_kind`.
5559    fn parse_yield(&mut self) -> Result<YieldStatement, ParseError> {
5560        let tok = self.consume(TokenType::Yield)?;
5561        let loc = self.loc_of(&tok);
5562        self.last_let_value_kind = "literal".to_string();
5563        let value_expr = self.parse_let_value_expr()?;
5564        Ok(YieldStatement {
5565            value_expr,
5566            value_kind: self.last_let_value_kind.clone(),
5567            loc,
5568        })
5569    }
5570
5571    /// Parse: keyword Name on target -> output_type (apply pattern).
5572    /// v2.67.0 — `compute <Name> on <a>, <b>, … -> <out>`.
5573    ///
5574    /// Positional arguments, bound to the compute's declared parameters in order.
5575    /// The generic [`Self::parse_apply_step`] captured a single `on <target>` and
5576    /// then the call site threw even that away (`arguments: Vec::new()`).
5577    fn parse_compute_apply(&mut self) -> Result<ComputeApplyStep, ParseError> {
5578        let tok = self.current().clone();
5579        let loc = self.loc_of(&tok);
5580        self.advance(); // consume `compute`
5581        let compute_name = self.consume_any_ident_or_kw()?.value.clone();
5582
5583        let mut arguments = Vec::new();
5584        if self.current().value == "on" {
5585            self.advance();
5586            loop {
5587                // v2.83.0 — SUBJECT position. README writes
5588                // `compute EligibilityScore on Profile.tenure, Profile.spend,
5589                // Profile.incidents -> score`; the bare-identifier read stopped
5590                // at the first dot, which is why every published `compute`
5591                // application failed on its own argument list.
5592                arguments.push(self.parse_subject()?);
5593                if self.check(TokenType::Comma) {
5594                    self.advance();
5595                } else {
5596                    break;
5597                }
5598            }
5599        }
5600
5601        let mut output_name = String::new();
5602        if self.check(TokenType::Arrow) {
5603            self.advance();
5604            output_name = self.consume_any_ident_or_kw()?.value.clone();
5605        }
5606
5607        Ok(ComputeApplyStep {
5608            compute_name,
5609            arguments,
5610            output_name,
5611            loc,
5612        })
5613    }
5614
5615    fn parse_apply_step(&mut self, _kw: &str) -> Result<(Loc, String, String, String), ParseError> {
5616        let tok = self.current().clone();
5617        self.advance(); // consume keyword
5618        let name = self.consume_any_ident_or_kw()?.value.clone();
5619        let mut target = String::new();
5620        let mut output_type = String::new();
5621        // "on" target
5622        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
5623            let next = self.current().clone();
5624            if next.value == "on" {
5625                self.advance();
5626                // v2.83.0 — SUBJECT position (the name before `on` is a
5627                // NAME and stays bare).
5628                target = self.parse_subject()?;
5629            }
5630        }
5631        // -> output_type
5632        if self.check(TokenType::Arrow) {
5633            self.advance();
5634            output_type = self.consume_any_ident_or_kw()?.value.clone();
5635        }
5636        // Skip optional braced block
5637        if self.check(TokenType::LBrace) {
5638            self.skip_braced_block()?;
5639        }
5640        Ok((
5641            Loc {
5642                line: tok.line,
5643                column: tok.column,
5644            },
5645            name,
5646            target,
5647            output_type,
5648        ))
5649    }
5650
5651    /// v2.83.0 — `<kind> <Name> [on <target>] [-> <binding>]` inside
5652    /// a `step { }` body.
5653    ///
5654    /// Differences from the flow-level `parse_apply_step`, both deliberate:
5655    ///
5656    /// - The target may be a CALL EXPRESSION, captured verbatim: README block
5657    ///   42 writes `mandate LegalPrecision on ContractDrafter(terms)`. The
5658    ///   flow-level form never needed this; the published step-level form does.
5659    /// - No trailing braced block is skipped. A guard is one statement; a
5660    /// silently-skipped block after it would be the v2.83.0 defect again.
5661    fn parse_step_guard(&mut self, kind: &str) -> Result<StepGuardNode, ParseError> {
5662        let tok = self.current().clone();
5663        self.advance(); // consume the keyword
5664        let name = self.consume_any_ident_or_kw()?.value.clone();
5665        let mut target = String::new();
5666        let mut binding = String::new();
5667        if self.current().value == "on" {
5668            self.advance();
5669            // v2.83.0 — SUBJECT position. `shield S on vital_event -> safe`
5670            // already worked; `shield S on Charge.output -> x` did not.
5671            target = self.parse_subject()?;
5672            // `ContractDrafter(terms)` — capture the balanced argument list
5673            // verbatim into the target string.
5674            if self.check(TokenType::LParen) {
5675                let mut depth = 0usize;
5676                loop {
5677                    let t = self.current().clone();
5678                    match t.ttype {
5679                        TokenType::LParen => depth += 1,
5680                        TokenType::RParen => depth -= 1,
5681                        TokenType::Eof => {
5682                            return Err(ParseError {
5683                                message: format!(
5684                                    "unterminated argument list in `{kind} {name} on {target}(…`"
5685                                ),
5686                                line: t.line,
5687                                column: t.column,
5688                                ..Default::default()
5689                            })
5690                        }
5691                        _ => {}
5692                    }
5693                    target.push_str(&t.value);
5694                    self.advance();
5695                    if depth == 0 {
5696                        break;
5697                    }
5698                }
5699            }
5700        }
5701        if self.check(TokenType::Arrow) {
5702            self.advance();
5703            binding = self.consume_any_ident_or_kw()?.value.clone();
5704        }
5705        Ok(StepGuardNode {
5706            kind: kind.to_string(),
5707            name,
5708            target,
5709            binding,
5710            loc: Loc {
5711                line: tok.line,
5712                column: tok.column,
5713            },
5714        })
5715    }
5716
5717    /// v2.83.0 — `reason [<target>] [{ given: … ask: "…" depth: N }]`.
5718    ///
5719    /// Replaces the `parse_flow_step_simple("reason")` call whose entire
5720    /// treatment of the block was `skip_braced_block()`. Sixteen README blocks
5721    /// write the braced form and every one of them lowered to an empty prompt.
5722    ///
5723    /// The field set is CLOSED. An unrecognised key is an ERROR that names the
5724    /// key and lists what is accepted — the v2.83.0 discipline: a skipped
5725    /// field in a deliberation removes the deliberation (a promptless `reason`
5726    /// is silent, not loud), so the silent direction is the dangerous one.
5727    fn parse_reason_step(&mut self) -> Result<ReasonStep, ParseError> {
5728        let tok = self.current().clone();
5729        let loc = self.loc_of(&tok);
5730        self.advance(); // consume `reason`
5731
5732        // The pre-v2.83.0 positional form: `reason <target>`. Absent when the
5733        // block follows immediately, which is how the README always writes it.
5734        //
5735        // The `Colon` lookahead matters: a bare `reason` on its own line inside
5736        // a `step { }` body is followed by the step's NEXT FIELD, and without
5737        // this guard the target would swallow that field's key (`output`) and
5738        // the step would then fail on a stray `:` — an error pointing two
5739        // tokens past the actual problem. `skip_flow_step_structural` used to
5740        // absorb this shape silently; a wrong diagnostic is not an improvement
5741        // on a silent drop.
5742        let next_is_field_key = self
5743            .tokens
5744            .get(self.pos + 1)
5745            .is_some_and(|t| t.ttype == TokenType::Colon);
5746        let target = if self.check(TokenType::LBrace)
5747            || self.at_declaration_start()
5748            || self.check(TokenType::RBrace)
5749            || self.check(TokenType::Eof)
5750            || next_is_field_key
5751        {
5752            String::new()
5753        } else {
5754            self.parse_dotted_identifier()?
5755        };
5756
5757        let mut node = ReasonStep {
5758            strategy: String::new(),
5759            target,
5760            given: String::new(),
5761            ask: String::new(),
5762            depth: None,
5763            loc,
5764        };
5765
5766        if !self.check(TokenType::LBrace) {
5767            return Ok(node);
5768        }
5769        self.advance();
5770        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5771            let key = self.current().clone();
5772            self.advance();
5773            self.consume(TokenType::Colon)?;
5774            match key.value.as_str() {
5775                // `given: A.output`, `given: A.output, sessions`,
5776                // `given: [baseline.topology, current.topology]` — all three
5777                // published shapes, normalised to one comma-joined string (the
5778                // same carrier `StepNode.given` already uses).
5779                "given" => {
5780                    let mut parts = vec![self.parse_expression_string()?];
5781                    while self.check(TokenType::Comma) {
5782                        self.advance();
5783                        parts.push(self.parse_expression_string()?);
5784                    }
5785                    node.given = parts.join(", ");
5786                }
5787                "ask" => node.ask = self.consume(TokenType::StringLit)?.value,
5788                "depth" => {
5789                    let n = self.current().clone();
5790                    if n.ttype != TokenType::Integer {
5791                        return Err(ParseError {
5792                            message: format!(
5793                                "`depth:` in a `reason` block is a deliberation depth — a \
5794                                 positive integer (got '{}')",
5795                                n.value
5796                            ),
5797                            line: n.line,
5798                            column: n.column,
5799                            ..Default::default()
5800                        });
5801                    }
5802                    self.advance();
5803                    node.depth = n.value.parse::<u32>().ok();
5804                }
5805                // `chain_of_thought: enabled` is the README's spelling of a
5806                // named strategy; `strategy: <name>` is the general form. Both
5807                // land in the same field because dispatch reads one posture.
5808                "chain_of_thought" => {
5809                    let v = self.consume_any_ident_or_kw()?.value;
5810                    if v == "enabled" {
5811                        node.strategy = "chain_of_thought".to_string();
5812                    }
5813                }
5814                "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value,
5815                // `target:` is the SUBJECT — the same field the positional
5816                // `reason <target>` form fills, spelled as a key. The parity
5817                // corpus writes it (`reason about_policy { target: "…" }`) and
5818                // the block was discarded whole, so the key has never meant
5819                // anything. Giving it BOTH ways is refused rather than resolved
5820                // by fiat: two spellings of one field with different values
5821                // have no defined winner, and picking one silently is how a
5822                // program comes to mean something its author did not write.
5823                "target" => {
5824                    let v = if self.check(TokenType::StringLit) {
5825                        self.consume(TokenType::StringLit)?.value
5826                    } else {
5827                        self.parse_dotted_identifier()?
5828                    };
5829                    if !node.target.is_empty() {
5830                        return Err(ParseError {
5831                            message: format!(
5832                                "`reason {} {{ target: … }}` declares the subject twice — \
5833                                 once positionally as `{}` and once as `target: {}`. They \
5834                                 are the same field. Write one of them.",
5835                                node.target, node.target, v
5836                            ),
5837                            line: key.line,
5838                            column: key.column,
5839                            ..Default::default()
5840                        });
5841                    }
5842                    node.target = v;
5843                }
5844                other => {
5845                    return Err(ParseError {
5846                        message: format!(
5847                            "unknown field '{other}' in a `reason` block. Accepted: given, \
5848                             ask, depth, strategy, chain_of_thought, target. A field this \
5849                             block does not recognise is REFUSED rather than skipped — a \
5850                             `reason` that silently loses its `ask:` deliberates over \
5851                             nothing, and that failure is quiet."
5852                        ),
5853                        line: key.line,
5854                        column: key.column,
5855                        ..Default::default()
5856                    })
5857                }
5858            }
5859        }
5860        self.consume(TokenType::RBrace)?;
5861        Ok(node)
5862    }
5863
5864    /// v2.83.0 — the CLOSED braceless catalog for `weave`.
5865    ///
5866    /// `output` is deliberately ABSENT, for the reason `at_navigate_field`
5867    /// already records: in step-body position `output:` is the STEP's own
5868    /// field, and a shared name makes the terminator ambiguous. This is not
5869    /// hypothetical here — it is the exact bug the old skipper had, from the
5870    /// other side: `skip_flow_step_structural` STOPPED at `output`, mid-list,
5871    /// and the step then failed on a stray comma.
5872    fn at_weave_field(&self) -> bool {
5873        const FIELDS: &[&str] = &["format", "include", "priority", "style"];
5874        self.field_ahead(FIELDS)
5875    }
5876
5877    /// v2.83.0 — `weave [a, b] [into <T>] [format: … include: […]]`.
5878    ///
5879    /// Three published surfaces, one implementation:
5880    ///   - the step-body statement — `weave [A.output, B.output]` followed by a
5881    ///     braceless `format:` / `include:` list (14 README blocks);
5882    ///   - the flow-body statement — `weave [A, B] into Report { format: T }`;
5883    ///   - the braced field form `weave { sources: […] … }`, which no published
5884    /// block writes but which predates this cycle and keeps working.
5885    fn parse_weave_step(&mut self) -> Result<FlowStep, ParseError> {
5886        let tok = self.current().clone();
5887        self.advance();
5888        let mut node = WeaveStep {
5889            sources: Vec::new(),
5890            target: String::new(),
5891            format_type: String::new(),
5892            priority: Vec::new(),
5893            style: String::new(),
5894            include: Vec::new(),
5895            loc: Loc {
5896                line: tok.line,
5897                column: tok.column,
5898            },
5899        };
5900        // `weave [A.output, B.output]` — the sources are REFERENCES, so they
5901        // are dotted. `parse_bracketed_dot_identifiers` is the same helper
5902        // `given:` uses; the pre-v2.83.0 braced form's `sources:` used the
5903        // non-dotted one, which is why a dotted source never had a spelling
5904        // that reached the AST.
5905        if self.check(TokenType::LBracket) {
5906            node.sources = self.parse_bracketed_dot_identifiers()?;
5907        } else if self.current().ttype == TokenType::Identifier
5908            && !self
5909                .tokens
5910                .get(self.pos + 1)
5911                .is_some_and(|t| t.ttype == TokenType::Colon)
5912        {
5913            // `weave Baz` — the bare positional subject every other statement
5914            // in the language takes (`probe X`, `reason X`, `validate X`), read
5915            // here as a one-element source list. It is the uniform rule, not a
5916            // special case, and it keeps parsing the shape that used to vanish
5917            // into `skip_flow_step_structural`.
5918            //
5919            // The Colon lookahead is the same guard `parse_reason_step` needs:
5920            // without it a bare `weave` would swallow the enclosing step's next
5921            // field KEY as its source.
5922            node.sources = vec![self.parse_dotted_identifier()?];
5923        }
5924        // `into <Target>` — the flow-level form's destination binding.
5925        if self.check(TokenType::Into) || self.current().value == "into" {
5926            self.advance();
5927            node.target = self.parse_dotted_identifier()?;
5928        }
5929        // The braceless continuation, terminated by the closed field catalog.
5930        while self.at_weave_field() {
5931            let f = self.current().value.clone();
5932            self.advance();
5933            self.consume(TokenType::Colon)?;
5934            match f.as_str() {
5935                "format" => node.format_type = self.consume_any_ident_or_kw()?.value.clone(),
5936                "include" => node.include = self.parse_bracketed_dot_identifiers()?,
5937                "priority" => node.priority = self.parse_bracketed_dot_identifiers()?,
5938                "style" => node.style = self.consume_any_ident_or_kw()?.value.clone(),
5939                // `at_weave_field` is the gate above; this arm is unreachable
5940                // unless the two catalogs drift apart.
5941                other => {
5942                    return Err(ParseError {
5943                        message: format!(
5944                            "`{other}` passed the `weave` field test but has no handler — \
5945                             the braceless catalog and its parser have drifted apart."
5946                        ),
5947                        line: tok.line,
5948                        column: tok.column,
5949                        ..Default::default()
5950                    })
5951                }
5952            }
5953        }
5954        if self.check(TokenType::LBrace) {
5955            self.advance();
5956            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5957                let f = self.current().value.clone();
5958                self.advance();
5959                if self.check(TokenType::Colon) {
5960                    self.advance();
5961                    match f.as_str() {
5962                        "sources" => node.sources = self.parse_bracketed_dot_identifiers()?,
5963                        "target" => node.target = self.consume_any_ident_or_kw()?.value.clone(),
5964                        "format" => {
5965                            node.format_type = self.consume_any_ident_or_kw()?.value.clone()
5966                        }
5967                        "priority" => node.priority = self.parse_bracketed_dot_identifiers()?,
5968                        "style" => node.style = self.consume_any_ident_or_kw()?.value.clone(),
5969                        // v2.83.0 — the braced form takes `include:` too,
5970                        // so the two spellings of one construct cannot disagree
5971                        // about which fields exist.
5972                        "include" => node.include = self.parse_bracketed_dot_identifiers()?,
5973                        _ => self.skip_value(),
5974                    }
5975                }
5976            }
5977            if self.check(TokenType::RBrace) {
5978                self.advance();
5979            }
5980        }
5981        Ok(FlowStep::Weave(node))
5982    }
5983
5984    fn parse_use_step(&mut self) -> Result<FlowStep, ParseError> {
5985        let tok = self.current().clone();
5986        self.advance();
5987        let tool_name = self.consume_any_ident_or_kw()?.value.clone();
5988        // v2.8.0 — two mutually-exclusive `use` argument surfaces:
5989        //   * `use Tool(query = "${q}", max_results = 5)` — D2 canonical
5990        // multi-field keyword args (v2.8.0 `UseArgs::Named`).
5991        // * `use Tool on "${arg}"` / `on query` — the v2.7.0 single positional
5992        //     argument (D5 back-compat, `UseArgs::LegacyPositional`):
5993        //       - a STRING LITERAL carrying interpolation (`on "${query}"`)
5994        //         resolved at dispatch against request-bound flow params;
5995        //       - a BARE identifier / literal (`on query` / `on 42`) verbatim.
5996        //     (Unquoted `${query}` is intentionally NOT a form — interpolation
5997        //     lives inside string literals everywhere in Axon.)
5998        let args = if self.check(TokenType::LParen) {
5999            UseArgs::Named(self.parse_named_arg_list()?)
6000        } else {
6001            let mut argument = String::new();
6002            if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6003                let next = self.current().clone();
6004                if next.value == "on" {
6005                    self.advance();
6006                    argument = self.consume_any_ident_or_kw()?.value.clone();
6007                }
6008            }
6009            UseArgs::LegacyPositional(argument)
6010        };
6011        if self.check(TokenType::LBrace) {
6012            self.skip_braced_block()?;
6013        }
6014        Ok(FlowStep::UseTool(UseToolStep {
6015            tool_name,
6016            args,
6017            loc: Loc {
6018                line: tok.line,
6019                column: tok.column,
6020            },
6021        }))
6022    }
6023
6024    /// v2.8.0 — parse `(name = value, …)` keyword args for the canonical
6025    /// `use Tool(...)` multi-field dispatch. Values are captured as expression
6026    /// strings (StringLit / Integer / Float / Bool / dotted identifier / list)
6027    /// via the shared `parse_let_atom`, since the frontend has no structured
6028    /// `Expr`. A trailing comma is tolerated; `()` yields no args.
6029    fn parse_named_arg_list(&mut self) -> Result<Vec<(String, String, String)>, ParseError> {
6030        self.consume(TokenType::LParen)?;
6031        let mut args = Vec::new();
6032        while !self.check(TokenType::RParen) {
6033            // Accept a keyword-as-name (`filter`, `type`, `from`, …) — real
6034            // adopter schemas use such names; the following `=` disambiguates.
6035            let name = self.consume_any_ident_or_kw()?.value;
6036            self.consume(TokenType::Assign)?;
6037            let value = self.parse_let_atom()?;
6038            // v2.10.0 — `parse_let_atom` classified the value (`"literal"` vs
6039            // `"reference"`); carry it so the runtime resolves a bare
6040            // identifier / `Step.output` as a binding lookup, not a literal.
6041            let value_kind = self.last_let_value_kind.clone();
6042            args.push((name, value, value_kind));
6043            if self.check(TokenType::Comma) {
6044                self.advance();
6045            } else {
6046                break;
6047            }
6048        }
6049        self.consume(TokenType::RParen)?;
6050        Ok(args)
6051    }
6052
6053    fn parse_remember_step(&mut self) -> Result<FlowStep, ParseError> {
6054        let tok = self.current().clone();
6055        self.advance();
6056        let expr = self.consume_any_ident_or_kw()?.value.clone();
6057        let mut mem = String::new();
6058        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6059            let next = self.current().clone();
6060            if next.value == "in" || next.ttype == TokenType::In {
6061                self.advance();
6062                mem = self.consume_any_ident_or_kw()?.value.clone();
6063            }
6064        }
6065        Ok(FlowStep::Remember(RememberStep {
6066            expression: expr,
6067            memory_target: mem,
6068            loc: Loc {
6069                line: tok.line,
6070                column: tok.column,
6071            },
6072        }))
6073    }
6074
6075    fn parse_recall_step(&mut self) -> Result<FlowStep, ParseError> {
6076        let tok = self.current().clone();
6077        self.advance();
6078        let query = if self.check(TokenType::StringLit) {
6079            self.consume(TokenType::StringLit)?.value.clone()
6080        } else {
6081            self.consume_any_ident_or_kw()?.value.clone()
6082        };
6083        let mut mem = String::new();
6084        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6085            let next = self.current().clone();
6086            if next.value == "from" || next.ttype == TokenType::From {
6087                self.advance();
6088                mem = self.consume_any_ident_or_kw()?.value.clone();
6089            }
6090        }
6091        Ok(FlowStep::Recall(RecallStep {
6092            query,
6093            memory_source: mem,
6094            loc: Loc {
6095                line: tok.line,
6096                column: tok.column,
6097            },
6098        }))
6099    }
6100
6101    fn parse_hibernate_step(&mut self) -> Result<FlowStep, ParseError> {
6102        let tok = self.current().clone();
6103        self.advance();
6104        let mut event = String::new();
6105        let mut timeout = String::new();
6106        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6107            // v2.83.0 — README III writes `hibernate until "event_name"`
6108            // (the `until` keyword + a STRING event). The parser accepted only
6109            // the bare-identifier form, so the published block never compiled.
6110            // Both forms resolve to the same field.
6111            let first = self.consume_any_ident_or_kw()?.value.clone();
6112            if first == "until" && self.check(TokenType::StringLit) {
6113                event = self.consume(TokenType::StringLit)?.value.clone();
6114            } else {
6115                event = first;
6116            }
6117        }
6118        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6119            let next = self.current().clone();
6120            if next.ttype == TokenType::Duration {
6121                self.advance();
6122                timeout = next.value.clone();
6123            }
6124        }
6125        Ok(FlowStep::Hibernate(HibernateStep {
6126            event_name: event,
6127            timeout,
6128            loc: Loc {
6129                line: tok.line,
6130                column: tok.column,
6131            },
6132        }))
6133    }
6134
6135    /// v2.63.0 — `focus <Dataspace> { where: "<filter>", select: [cols], as: <name> }`
6136    /// — σ_φ ∘ π_v over a declared dataspace. The `where:` string is the
6137    /// v1.30.0 data-plane filter grammar (the design decision, shared with retrieve /
6138    /// navigate). Pre-108.d the optional body was silently discarded.
6139    /// v2.65.0 — `grad <letName> wrt <x> [as <name>]` /
6140    /// `grad <letName> wrt [a, b] as <name>`. The differentiation itself
6141    /// happens at CHECK/IR time (T931/T932 + the symbolic differentiator);
6142    /// the parser only captures the surface.
6143    fn parse_grad_step(&mut self) -> Result<FlowStep, ParseError> {
6144        let tok = self.current().clone();
6145        self.advance();
6146        let target = self.consume_any_ident_or_kw()?.value.clone();
6147        let mut wrt: Vec<String> = Vec::new();
6148        let mut output = String::new();
6149        if !self.at_declaration_start() && self.current().value == "wrt" {
6150            self.advance();
6151            if self.check(TokenType::LBracket) {
6152                wrt = self.parse_bracketed_identifiers()?;
6153            } else {
6154                wrt.push(self.consume_any_ident_or_kw()?.value.clone());
6155            }
6156        }
6157        if !self.at_declaration_start() && self.current().value == "as" {
6158            self.advance();
6159            output = self.consume_any_ident_or_kw()?.value.clone();
6160        }
6161        Ok(FlowStep::Grad(GradStep {
6162            target,
6163            wrt,
6164            output,
6165            loc: Loc {
6166                line: tok.line,
6167                column: tok.column,
6168            },
6169        }))
6170    }
6171
6172    fn parse_focus_step(&mut self) -> Result<FlowStep, ParseError> {
6173        let tok = self.current().clone();
6174        self.advance();
6175        let expression = if self.at_declaration_start()
6176            || self.check(TokenType::RBrace)
6177            || self.check(TokenType::Eof)
6178        {
6179            String::new()
6180        } else {
6181            self.consume_any_ident_or_kw()?.value.clone()
6182        };
6183        let mut where_expr = String::new();
6184        let mut select: Vec<String> = Vec::new();
6185        let mut output = String::new();
6186        if self.check(TokenType::LBrace) {
6187            self.advance();
6188            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6189                if self.check(TokenType::Comma) {
6190                    self.advance();
6191                    continue;
6192                }
6193                let f = self.current().value.clone();
6194                self.advance();
6195                if self.check(TokenType::Colon) {
6196                    self.advance();
6197                    match f.as_str() {
6198                        "where" => {
6199                            where_expr = self.consume(TokenType::StringLit)?.value.clone()
6200                        }
6201                        "select" => select = self.parse_bracketed_identifiers()?,
6202                        "as" | "alias" => {
6203                            output = self.consume_any_ident_or_kw()?.value.clone()
6204                        }
6205                        _ => self.skip_value(),
6206                    }
6207                }
6208            }
6209            if self.check(TokenType::RBrace) {
6210                self.advance();
6211            }
6212        }
6213        Ok(FlowStep::Focus(FocusStep {
6214            expression,
6215            where_expr,
6216            select,
6217            output,
6218            loc: Loc {
6219                line: tok.line,
6220                column: tok.column,
6221            },
6222        }))
6223    }
6224
6225    fn parse_associate_step(&mut self) -> Result<FlowStep, ParseError> {
6226        let tok = self.current().clone();
6227        self.advance();
6228        let left = self.consume_any_ident_or_kw()?.value.clone();
6229        let mut right = String::new();
6230        let mut using = String::new();
6231        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6232            right = self.consume_any_ident_or_kw()?.value.clone();
6233        }
6234        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6235            let next = self.current().clone();
6236            if next.value == "using" {
6237                self.advance();
6238                using = self.consume_any_ident_or_kw()?.value.clone();
6239            }
6240        }
6241        let mut output = String::new();
6242        if self.check(TokenType::LBrace) {
6243            self.advance();
6244            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6245                let f = self.current().value.clone();
6246                self.advance();
6247                if self.check(TokenType::Colon) {
6248                    self.advance();
6249                    match f.as_str() {
6250                        "as" | "alias" => output = self.consume_any_ident_or_kw()?.value.clone(),
6251                        _ => self.skip_value(),
6252                    }
6253                }
6254            }
6255            if self.check(TokenType::RBrace) {
6256                self.advance();
6257            }
6258        }
6259        Ok(FlowStep::Associate(AssociateStep {
6260            left,
6261            right,
6262            using_field: using,
6263            output,
6264            loc: Loc {
6265                line: tok.line,
6266                column: tok.column,
6267            },
6268        }))
6269    }
6270
6271    fn parse_aggregate_step(&mut self) -> Result<FlowStep, ParseError> {
6272        let tok = self.current().clone();
6273        self.advance();
6274        let target = self.consume_any_ident_or_kw()?.value.clone();
6275        let mut group_by = Vec::new();
6276        let mut alias = String::new();
6277        let mut compute: Vec<String> = Vec::new();
6278        let mut where_expr = String::new();
6279        if self.check(TokenType::LBrace) {
6280            self.advance();
6281            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6282                let f = self.current().value.clone();
6283                self.advance();
6284                if self.check(TokenType::Colon) {
6285                    self.advance();
6286                    match f.as_str() {
6287                        "group_by" => group_by = self.parse_bracketed_identifiers()?,
6288                        "alias" | "as" => alias = self.consume_any_ident_or_kw()?.value.clone(),
6289                        // v2.63.0 — the closed aggregate catalog, kept
6290                        // RAW (`count`, `sum(score)`, …); T930 validates.
6291                        "compute" => compute = self.parse_bracketed_aggregates()?,
6292                        // v2.63.0 — the data-plane where.
6293                        "where" => where_expr = self.consume(TokenType::StringLit)?.value.clone(),
6294                        _ => self.skip_value(),
6295                    }
6296                }
6297            }
6298            if self.check(TokenType::RBrace) {
6299                self.advance();
6300            }
6301        }
6302        Ok(FlowStep::Aggregate(AggregateStep {
6303            target,
6304            group_by,
6305            alias,
6306            compute,
6307            where_expr,
6308            loc: Loc {
6309                line: tok.line,
6310                column: tok.column,
6311            },
6312        }))
6313    }
6314
6315    fn parse_explore_step(&mut self) -> Result<FlowStep, ParseError> {
6316        let tok = self.current().clone();
6317        self.advance();
6318        let target = self.consume_any_ident_or_kw()?.value.clone();
6319        let mut limit = None;
6320        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6321            if self.current().ttype == TokenType::Integer {
6322                limit = self.current().value.parse::<i64>().ok();
6323                self.advance();
6324            }
6325        }
6326        let mut output = String::new();
6327        if self.check(TokenType::LBrace) {
6328            self.advance();
6329            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6330                let f = self.current().value.clone();
6331                self.advance();
6332                if self.check(TokenType::Colon) {
6333                    self.advance();
6334                    match f.as_str() {
6335                        "as" | "alias" => output = self.consume_any_ident_or_kw()?.value.clone(),
6336                        _ => self.skip_value(),
6337                    }
6338                }
6339            }
6340            if self.check(TokenType::RBrace) {
6341                self.advance();
6342            }
6343        }
6344        Ok(FlowStep::ExploreStep(ExploreStepNode {
6345            target,
6346            limit,
6347            output,
6348            loc: Loc {
6349                line: tok.line,
6350                column: tok.column,
6351            },
6352        }))
6353    }
6354
6355    /// v2.63.0 — parse `[count, sum(score), avg(x)]`: bracketed
6356    /// aggregate entries, each `ident` or `ident(ident)`, kept raw.
6357    fn parse_bracketed_aggregates(&mut self) -> Result<Vec<String>, ParseError> {
6358        let mut out = Vec::new();
6359        self.consume(TokenType::LBracket)?;
6360        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
6361            let name = self.consume_any_ident_or_kw()?.value.clone();
6362            if self.check(TokenType::LParen) {
6363                self.advance();
6364                let col = self.consume_any_ident_or_kw()?.value.clone();
6365                self.consume(TokenType::RParen)?;
6366                out.push(format!("{name}({col})"));
6367            } else {
6368                out.push(name);
6369            }
6370            if self.check(TokenType::Comma) {
6371                self.advance();
6372            }
6373        }
6374        self.consume(TokenType::RBracket)?;
6375        Ok(out)
6376    }
6377
6378    /// v2.63.0 — the governed ingest step:
6379    ///
6380    /// ```text
6381    /// ingest <sourceRef> into <Dataspace> {
6382    ///     format: csv | json
6383    ///     limits { max_bytes: N, max_rows: N }
6384    /// }
6385    /// ```
6386    ///
6387    /// Until 108.c the body was consumed by `skip_braced_block()`. Now it
6388    /// is a closed grammar: `format:` (raw here; required + validated by
6389    /// `axon-T929`) and an optional `limits { … }` block whose bounds are
6390    /// enforced on the raw byte stream BEFORE parsing. An unknown
6391    /// body entry is a parse error.
6392    fn parse_ingest_step(&mut self) -> Result<FlowStep, ParseError> {
6393        let tok = self.current().clone();
6394        self.advance();
6395        let source = self.consume_any_ident_or_kw()?.value.clone();
6396        let mut target = String::new();
6397        let mut format = String::new();
6398        let mut max_bytes: Option<u64> = None;
6399        let mut max_rows: Option<u64> = None;
6400        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6401            let next = self.current().clone();
6402            if next.value == "into" || next.ttype == TokenType::Into {
6403                self.advance();
6404                target = self.consume_any_ident_or_kw()?.value.clone();
6405            }
6406        }
6407        if self.check(TokenType::LBrace) {
6408            self.consume(TokenType::LBrace)?;
6409            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6410                // Optional separators between body entries.
6411                if self.check(TokenType::Comma) {
6412                    self.advance();
6413                    continue;
6414                }
6415                let entry = self.current().clone();
6416                match entry.value.as_str() {
6417                    "format" => {
6418                        self.advance();
6419                        self.consume(TokenType::Colon)?;
6420                        format = self.consume_any_ident_or_kw()?.value.clone();
6421                    }
6422                    "limits" => {
6423                        self.advance();
6424                        self.consume(TokenType::LBrace)?;
6425                        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6426                            let bound = self.current().clone();
6427                            self.advance();
6428                            self.consume(TokenType::Colon)?;
6429                            let num_tok = self.consume(TokenType::Integer)?.clone();
6430                            let value = num_tok.value.parse::<u64>().map_err(|_| ParseError {
6431                                message: format!(
6432                                    "ingest `limits` bound `{}` must be a non-negative \
6433                                     integer byte/row count, got `{}`.",
6434                                    bound.value, num_tok.value
6435                                ),
6436                                line: num_tok.line,
6437                                column: num_tok.column,
6438                                ..Default::default()
6439                            })?;
6440                            match bound.value.as_str() {
6441                                "max_bytes" => max_bytes = Some(value),
6442                                "max_rows" => max_rows = Some(value),
6443                                other => {
6444                                    return Err(ParseError {
6445                                        message: format!(
6446                                            "Unknown ingest limit `{other}`. The closed \
6447                                             limits grammar is `max_bytes: <N>` and \
6448                                             `max_rows: <N>` — bounds enforced on the raw \
6449                                             stream BEFORE parsing.",
6450                                        ),
6451                                        line: bound.line,
6452                                        column: bound.column,
6453                                        ..Default::default()
6454                                    });
6455                                }
6456                            }
6457                            if self.check(TokenType::Comma) {
6458                                self.advance();
6459                            }
6460                        }
6461                        self.consume(TokenType::RBrace)?;
6462                    }
6463                    other => {
6464                        return Err(ParseError {
6465                            message: format!(
6466                                "Unknown entry `{other}` in ingest body. The closed \
6467                                 grammar is `format: csv|json` and \
6468                                 `limits {{ max_bytes: <N>, max_rows: <N> }}`.",
6469                            ),
6470                            line: entry.line,
6471                            column: entry.column,
6472                            ..Default::default()
6473                        });
6474                    }
6475                }
6476            }
6477            self.consume(TokenType::RBrace)?;
6478        }
6479        Ok(FlowStep::Ingest(IngestStep {
6480            source,
6481            target,
6482            format,
6483            max_bytes,
6484            max_rows,
6485            loc: Loc {
6486                line: tok.line,
6487                column: tok.column,
6488            },
6489        }))
6490    }
6491
6492    /// v2.83.0 — is the cursor on a `navigate` field (`<name>:`)?
6493    ///
6494    /// The continuation test for the braceless field list. Closed catalog by
6495    /// construction: a name outside it ends the navigate and belongs to the
6496    /// enclosing step, which is exactly what makes the delimiter-free form
6497    /// unambiguous.
6498    fn at_navigate_field(&self) -> bool {
6499        const FIELDS: &[&str] = &[
6500            // v2.83.0 — `output` is deliberately ABSENT from the
6501            // BRACELESS catalog even though the braced form accepts it as an
6502            // alias for `as`. In step-body position `output:` is the STEP's
6503            // own field, and a shared name would make the terminator
6504            // ambiguous — the braceless navigate would swallow the step's
6505            // output type. README writes `as:` in this position throughout;
6506            // the braced/flow-level form keeps both spellings.
6507            "corpus", "query", "trail", "as", "from", "budget", "where",
6508            "depth", "recall",
6509        ];
6510        self.field_ahead(FIELDS)
6511    }
6512
6513    /// v2.83.0 — the same test for `drill`.
6514    fn at_drill_field(&self) -> bool {
6515        // Same reason as `at_navigate_field`: no `output` in the braceless
6516        // catalog, because that name belongs to the enclosing step.
6517        const FIELDS: &[&str] = &["subtree", "path", "query", "as"];
6518        self.field_ahead(FIELDS)
6519    }
6520
6521    /// `<one of names>` immediately followed by `:`.
6522    fn field_ahead(&self, names: &[&str]) -> bool {
6523        let cur = self.current();
6524        if !names.contains(&cur.value.as_str()) {
6525            return false;
6526        }
6527        self.tokens
6528            .get(self.pos + 1)
6529            .is_some_and(|t| t.ttype == TokenType::Colon)
6530    }
6531
6532    /// v2.83.0 — a CONFIG KEY: `"env:DATABASE_URL"` or the bare
6533    /// `env:DATABASE_URL` README publishes.
6534    ///
6535    /// v2.67.0 made `connection:`/`endpoint:` a config KEY rather than a URL or a
6536    /// DSN — the address resolves per deployment. README writes both the
6537    /// quoted and the bare spelling; the parser took only the quoted one, so
6538    /// every published `axonstore` with an unquoted key failed on its own
6539    /// third line. One value, two spellings — the epsilon/tolerance
6540    /// resolution of v2.83.0, applied to the config surface.
6541    fn parse_config_key(&mut self) -> Result<String, ParseError> {
6542        if self.check(TokenType::StringLit) {
6543            return Ok(self.consume(TokenType::StringLit)?.value.clone());
6544        }
6545        let scheme = self.consume_any_ident_or_kw()?.value.clone();
6546        if self.check(TokenType::Colon) {
6547            self.advance();
6548            let key = self.consume_any_ident_or_kw()?.value.clone();
6549            return Ok(format!("{scheme}:{key}"));
6550        }
6551        Ok(scheme)
6552    }
6553
6554    /// v2.83.0 — a PIX field value: a string literal OR a binding
6555    /// reference. README writes `query: question` (the flow parameter) far
6556    /// more often than a literal, and the parser accepted only the literal —
6557    /// which is why every published `navigate` failed on its own second line.
6558    fn parse_pix_value(&mut self) -> Result<String, ParseError> {
6559        if self.check(TokenType::StringLit) {
6560            return Ok(self.consume(TokenType::StringLit)?.value.clone());
6561        }
6562        Ok(self.consume_any_ident_or_kw()?.value.clone())
6563    }
6564
6565    fn parse_navigate_step(&mut self) -> Result<FlowStep, ParseError> {
6566        let tok = self.current().clone();
6567        self.advance();
6568        let pix_name = self.consume_any_ident_or_kw()?.value.clone();
6569        let mut node = NavigateStep {
6570            depth: None,
6571            pix_name,
6572            corpus_name: String::new(),
6573            query_expr: String::new(),
6574            trail_enabled: false,
6575            output_name: String::new(),
6576            seed: String::new(),
6577            budget: None,
6578            where_expr: String::new(),
6579            loc: Loc {
6580                line: tok.line,
6581                column: tok.column,
6582            },
6583        };
6584        // v2.83.0 — the BRACELESS field form, which is what README pix/
6585        // corpus publishes everywhere:
6586        //
6587        //     navigate ContractIndex
6588        //         query: question
6589        //         trail: enabled
6590        //         as: relevant_sections
6591        //
6592        // Terminated by the field-name set, not by a brace: the navigate
6593        // fields are a CLOSED catalog, so "the next token is one of these and
6594        // is followed by a colon" is an unambiguous continuation test. That is
6595        // the same closed-catalog discipline the rest of the language uses,
6596        // and it is why this form needs no delimiter to be parseable.
6597        if !self.check(TokenType::LBrace) {
6598            while self.at_navigate_field() {
6599                let f = self.current().value.clone();
6600                self.advance();
6601                self.consume(TokenType::Colon)?;
6602                match f.as_str() {
6603                    "corpus" => node.corpus_name = self.consume_any_ident_or_kw()?.value.clone(),
6604                    "query" => node.query_expr = self.parse_pix_value()?,
6605                    "trail" => {
6606                        let v = self.consume_any_ident_or_kw()?.value;
6607                        node.trail_enabled = matches!(v.as_str(), "true" | "enabled" | "on");
6608                    }
6609                    "output" | "as" => {
6610                        node.output_name = self.consume_any_ident_or_kw()?.value.clone()
6611                    }
6612                    "from" => node.seed = self.consume_any_ident_or_kw()?.value.clone(),
6613                    "budget" => node.budget = self.parse_optional_int(),
6614                    "where" => node.where_expr = self.parse_pix_value()?,
6615                    "depth" => node.depth = self.parse_optional_int(),
6616                    // v2.83.0 — `recall: episodic` selects the MDN memory
6617                    // mode README's clinical/legal examples write. The
6618                    // navigator's episodic path is v2.13.0's adaptive corpus
6619                    // reinforcement, keyed by the corpus declaration; the
6620                    // value is accepted and recorded on the seed so nothing
6621                    // is silently dropped, and the adaptive path already
6622                    // reads the corpus-level flag.
6623                    "recall" => {
6624                        let mode = self.consume_any_ident_or_kw()?.value.clone();
6625                        if node.seed.is_empty() {
6626                            node.seed = format!("recall:{mode}");
6627                        }
6628                    }
6629                    _ => self.skip_value(),
6630                }
6631            }
6632        }
6633        if self.check(TokenType::LBrace) {
6634            self.advance();
6635            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6636                let f = self.current().value.clone();
6637                self.advance();
6638                if self.check(TokenType::Colon) {
6639                    self.advance();
6640                    match f.as_str() {
6641                        "corpus" => {
6642                            node.corpus_name = self.consume_any_ident_or_kw()?.value.clone()
6643                        }
6644                        "query" => node.query_expr = self.parse_pix_value()?,
6645                        "trail" => {
6646                            let v = self.consume_any_ident_or_kw()?.value;
6647                            node.trail_enabled =
6648                                matches!(v.as_str(), "true" | "enabled" | "on");
6649                        }
6650                        "output" | "as" => {
6651                            node.output_name = self.consume_any_ident_or_kw()?.value.clone()
6652                        }
6653                        // v2.13.0 — MDN corpus-graph navigation.
6654                        "from" => node.seed = self.consume_any_ident_or_kw()?.value.clone(),
6655                        "budget" => node.budget = self.parse_optional_int(),
6656                        // v2.17.0 (Q2) — column-scoped navigation: a raw filter
6657                        // expr (mirrors `retrieve … where`) pushed to the SELECT
6658                        // that sources the corpus `documents:`/`relations:` rows,
6659                        // so a `corpus from axonstore` is scoped to a sub-tenant
6660                        // COLUMN (`where: "tenant_id == '${tenant_id}'"`), not just
6661                        // the axon-tenant RLS scope. Resolved by the v1.32.0 filter
6662                        // compiler at runtime (`${name}` → `$N` bind params).
6663                        "where" => {
6664                            node.where_expr = self.consume(TokenType::StringLit)?.value.clone()
6665                        }
6666                        _ => self.skip_value(),
6667                    }
6668                }
6669            }
6670            if self.check(TokenType::RBrace) {
6671                self.advance();
6672            }
6673        }
6674        Ok(FlowStep::Navigate(node))
6675    }
6676
6677    fn parse_drill_step(&mut self) -> Result<FlowStep, ParseError> {
6678        let tok = self.current().clone();
6679        self.advance();
6680        let pix_name = self.consume_any_ident_or_kw()?.value.clone();
6681        let mut node = DrillStep {
6682            pix_name,
6683            subtree_path: String::new(),
6684            query_expr: String::new(),
6685            output_name: String::new(),
6686            loc: Loc {
6687                line: tok.line,
6688                column: tok.column,
6689            },
6690        };
6691        // v2.83.0 — `drill <Ref> into "<path>" query: … as: …`, the form
6692        // README publishes. `into` is a positional keyword (no colon), the
6693        // rest is the same braceless closed-catalog field list as `navigate`.
6694        if self.current().value == "into" {
6695            self.advance();
6696            // v2.83.0 — README writes BOTH `into "Liabilities"` (a title)
6697            // and `into findings.top_region` (a dotted binding path). The
6698            // subtree path is dot-separated either way, so both spellings
6699            // land in the same field.
6700            node.subtree_path = if self.check(TokenType::StringLit) {
6701                self.consume(TokenType::StringLit)?.value.clone()
6702            } else {
6703                self.parse_dotted_identifier()?
6704            };
6705        }
6706        if !self.check(TokenType::LBrace) {
6707            while self.at_drill_field() {
6708                let f = self.current().value.clone();
6709                self.advance();
6710                self.consume(TokenType::Colon)?;
6711                match f.as_str() {
6712                    "subtree" | "path" => {
6713                        node.subtree_path = self.consume(TokenType::StringLit)?.value.clone()
6714                    }
6715                    "query" => node.query_expr = self.parse_pix_value()?,
6716                    "output" | "as" => {
6717                        node.output_name = self.consume_any_ident_or_kw()?.value.clone()
6718                    }
6719                    _ => self.skip_value(),
6720                }
6721            }
6722        }
6723        if self.check(TokenType::LBrace) {
6724            self.advance();
6725            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6726                let f = self.current().value.clone();
6727                self.advance();
6728                if self.check(TokenType::Colon) {
6729                    self.advance();
6730                    match f.as_str() {
6731                        "subtree" | "path" => {
6732                            node.subtree_path = self.consume(TokenType::StringLit)?.value.clone()
6733                        }
6734                        "query" => node.query_expr = self.parse_pix_value()?,
6735                        "output" | "as" => {
6736                            node.output_name = self.consume_any_ident_or_kw()?.value.clone()
6737                        }
6738                        _ => self.skip_value(),
6739                    }
6740                }
6741            }
6742            if self.check(TokenType::RBrace) {
6743                self.advance();
6744            }
6745        }
6746        Ok(FlowStep::Drill(node))
6747    }
6748
6749    fn parse_corroborate_step(&mut self) -> Result<FlowStep, ParseError> {
6750        let tok = self.current().clone();
6751        self.advance();
6752        let nav_ref = self.consume_any_ident_or_kw()?.value.clone();
6753        let mut output = String::new();
6754        if self.check(TokenType::Arrow) {
6755            self.advance();
6756            output = self.consume_any_ident_or_kw()?.value.clone();
6757        }
6758        Ok(FlowStep::Corroborate(CorroborateStep {
6759            navigate_ref: nav_ref,
6760            output_name: output,
6761            loc: Loc {
6762                line: tok.line,
6763                column: tok.column,
6764            },
6765        }))
6766    }
6767
6768    fn parse_listen_step(&mut self) -> Result<FlowStep, ParseError> {
6769        let tok = self.current().clone();
6770        self.advance();
6771        // v1.6.0 D4 — dual-mode listen:
6772        // • String topic (legacy, deprecated since v1.6.0)
6773        //   • Identifier (canonical: declared ChannelDefinition)
6774        let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
6775            (self.consume(TokenType::StringLit)?.value.clone(), false)
6776        } else {
6777            (self.consume_any_ident_or_kw()?.value.clone(), true)
6778        };
6779        let mut alias = String::new();
6780        if !self.at_declaration_start()
6781            && !self.check(TokenType::RBrace)
6782            && !self.check(TokenType::LBrace)
6783        {
6784            let next = self.current().clone();
6785            if next.value == "as" || next.ttype == TokenType::As {
6786                self.advance();
6787                alias = self.consume_any_ident_or_kw()?.value.clone();
6788            }
6789        }
6790        // v2.4.0 — parse the handler body into real flow-steps (was
6791        // `skip_braced_block`'d, leaving the listener inert). The body runs on
6792        // each event / scheduled tick.
6793        let body = self.parse_listener_body()?;
6794        Ok(FlowStep::Listen(ListenStep {
6795            channel,
6796            channel_is_ref,
6797            event_alias: alias,
6798            body,
6799            loc: Loc {
6800                line: tok.line,
6801                column: tok.column,
6802            },
6803        }))
6804    }
6805
6806    /// v2.4.0 — parse a `listen … { <flow steps> }` handler body. The body
6807    /// is OPTIONAL (a bodyless `listen channel` returns an empty Vec); when
6808    /// present, each statement is a real [`FlowStep`] (the same grammar as a
6809    /// flow / `quant` / `par` body), executed per trigger by the v2.4.0 runtime.
6810    fn parse_listener_body(&mut self) -> Result<Vec<FlowStep>, ParseError> {
6811        let mut body = Vec::new();
6812        if self.check(TokenType::LBrace) {
6813            self.advance(); // consume `{`
6814            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6815                body.push(self.parse_flow_step()?);
6816            }
6817            self.consume(TokenType::RBrace)?;
6818        }
6819        Ok(body)
6820    }
6821
6822    /// v2.83.0 — `retrieve [from] <Store> [where "<expr>"] [as <alias>]`
6823    /// alongside the pre-existing braced `retrieve <Store> { where: … as: … }`.
6824    ///
6825    /// README axonstore writes the braceless form with `from` and with `where`
6826    /// taking its argument DIRECTLY — no colon. Neither spelling parsed, so the
6827    /// only published `retrieve` failed on its own first line.
6828    fn parse_retrieve_step(&mut self) -> Result<FlowStep, ParseError> {
6829        let tok = self.current().clone();
6830        self.advance();
6831        // `from` is optional noise-with-meaning: it reads as English and the
6832        // store name carries the content either way.
6833        if self.check(TokenType::From) || self.current().value == "from" {
6834            self.advance();
6835        }
6836        let store = self.consume_any_ident_or_kw()?.value.clone();
6837        let mut where_expr = String::new();
6838        let mut alias = String::new();
6839        let mut order_by = String::new();
6840        let mut limit_expr = String::new();
6841        let mut aggregate = String::new();
6842        let mut group_by = String::new();
6843        let mut cache = String::new();
6844        // v2.83.0 — the BRACELESS clauses README publishes. Note they
6845        // take their argument with NO colon (`where "…"`, `as record`), which
6846        // is why the closed-catalog `field_ahead` test used elsewhere does not
6847        // apply: the terminator here is the clause keyword itself. Both names
6848        // are absent from the step-body field set, so a `retrieve` written
6849        // inside a step cannot swallow the step's own fields.
6850        loop {
6851            match self.current().value.as_str() {
6852                "where" if !self.check(TokenType::LBrace) => {
6853                    self.advance();
6854                    where_expr = self.consume(TokenType::StringLit)?.value.clone();
6855                }
6856                "as" => {
6857                    self.advance();
6858                    alias = self.consume_any_ident_or_kw()?.value.clone();
6859                }
6860                _ => break,
6861            }
6862        }
6863        if self.check(TokenType::LBrace) {
6864            self.advance();
6865            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6866                let f = self.current().value.clone();
6867                self.advance();
6868                if self.check(TokenType::Colon) {
6869                    self.advance();
6870                    match f.as_str() {
6871                        "where" => where_expr = self.consume(TokenType::StringLit)?.value.clone(),
6872                        "as" | "alias" => alias = self.consume_any_ident_or_kw()?.value.clone(),
6873                        // v2.21.0 — `order_by:` is a string literal
6874                        // (`"col asc, col2 desc"`), same surface as `where:`.
6875                        "order_by" => {
6876                            order_by = self.consume(TokenType::StringLit)?.value.clone()
6877                        }
6878                        // v2.21.0 — `limit:` is a bare integer literal
6879                        // (`limit: 100`) OR a string carrying a binding
6880                        // (`limit: "${max}"`). Captured raw; the runtime
6881                        // resolves + validates it as a `u32`.
6882                        "limit" => {
6883                            let t = self.current().clone();
6884                            match t.ttype {
6885                                TokenType::Integer | TokenType::StringLit => {
6886                                    limit_expr = t.value.clone();
6887                                    self.advance();
6888                                }
6889                                _ => self.skip_value(),
6890                            }
6891                        }
6892                        // v2.33.0 — `aggregate:` is a string literal from
6893                        // the CLOSED catalog (`"count"`, `"sum(tokens)"`, …);
6894                        // `group_by:` is a string literal listing columns
6895                        // (`"industry, status"`). Both captured raw; the
6896                        // v1.31.0 proof (axon-T843/T844/T845) + the runtime
6897                        // (`filter::parse_aggregate_clause`) validate.
6898                        "aggregate" => {
6899                            aggregate = self.consume(TokenType::StringLit)?.value.clone()
6900                        }
6901                        "group_by" => {
6902                            group_by = self.consume(TokenType::StringLit)?.value.clone()
6903                        }
6904                        // v2.40.0 — `cache:` names a declared `cache`
6905                        // policy. A retrieve reads a store (never `pure`), so
6906                        // caching it always accepts staleness — the checker
6907                        // requires a finite `ttl:` on the referenced cache
6908                        // (axon-T865) and resolves the reference (axon-T864).
6909                        "cache" => cache = self.consume_any_ident_or_kw()?.value.clone(),
6910                        _ => self.skip_value(),
6911                    }
6912                }
6913            }
6914            if self.check(TokenType::RBrace) {
6915                self.advance();
6916            }
6917        }
6918        Ok(FlowStep::Retrieve(RetrieveStep {
6919            store_name: store,
6920            where_expr,
6921            alias,
6922            order_by,
6923            limit_expr,
6924            aggregate,
6925            group_by,
6926            cache,
6927            loc: Loc {
6928                line: tok.line,
6929                column: tok.column,
6930            },
6931        }))
6932    }
6933
6934    /// v1.30.0 — Parse a `purge` step, capturing the optional
6935    /// `{ where: "<expr>" }` filter. (v1.30.0 moved `mutate` to its
6936    /// own `parse_mutate_step`, which also captures SET columns; this
6937    /// helper now serves `purge` alone — a `DELETE` has no SET clause.)
6938    ///
6939    /// Before v1.30.0 these two steps parsed via `parse_flow_step_simple`,
6940    /// which *skipped* the braced block — so a written `where:` clause
6941    /// was silently dropped and every `mutate`/`purge` ran against the
6942    /// whole store, leaving the entire v1.30.0 parameterized-filter
6943    /// machinery unreachable for them. This mirror of `parse_retrieve_step`
6944    /// (minus the `as:` alias — a mutate/purge binds no result) closes
6945    /// that gap. Returns `(loc, store_name, where_expr)`.
6946    fn parse_store_where_step(
6947        &mut self,
6948    ) -> Result<(Loc, String, String), ParseError> {
6949        let tok = self.current().clone();
6950        self.advance(); // consume the keyword
6951        let store = if self.at_declaration_start()
6952            || self.check(TokenType::RBrace)
6953            || self.check(TokenType::Eof)
6954        {
6955            String::new()
6956        } else {
6957            self.consume_any_ident_or_kw()?.value.clone()
6958        };
6959        let mut where_expr = String::new();
6960        if self.check(TokenType::LBrace) {
6961            self.advance();
6962            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6963                let field = self.current().value.clone();
6964                self.advance();
6965                if self.check(TokenType::Colon) {
6966                    self.advance();
6967                    match field.as_str() {
6968                        "where" => {
6969                            where_expr =
6970                                self.consume(TokenType::StringLit)?.value.clone()
6971                        }
6972                        _ => self.skip_value(),
6973                    }
6974                }
6975            }
6976            if self.check(TokenType::RBrace) {
6977                self.advance();
6978            }
6979        }
6980        Ok((
6981            Loc {
6982                line: tok.line,
6983                column: tok.column,
6984            },
6985            store,
6986            where_expr,
6987        ))
6988    }
6989
6990    /// v1.30.0 — Parse a `persist` step, capturing the optional
6991    /// `{ col: value }` field block.
6992    ///
6993    /// Before v1.30.0 `persist` parsed via `parse_flow_step_simple`,
6994    /// which *skipped* the braced block — so a written field block was
6995    /// silently dropped and the runtime fell back to writing every
6996    /// context binding as a row, which fails against any real table
6997    /// (flows always carry more bindings than a table has columns).
6998    /// This captures the declared columns into `PersistStep.fields`;
6999    /// the runtime writes exactly those (interpolated). A `persist`
7000    /// with no block keeps the v1.30.0 user-bindings fallback — fully
7001    /// backward-compatible. Mirror of `parse_retrieve_step`, but the
7002    /// keys are arbitrary column names rather than the fixed
7003    /// `where:` / `as:` filter keys.
7004    ///
7005    /// The optional `into` connector (`persist into <store>`) is
7006    /// accepted and skipped — before v1.30.0 `into` was captured as
7007    /// the store name.
7008    fn parse_persist_step(&mut self) -> Result<FlowStep, ParseError> {
7009        let tok = self.current().clone();
7010        self.advance(); // consume `persist`
7011        // Optional `into` connector — skip it so the store name that
7012        // follows is not mistaken for the target.
7013        if self.current().value == "into" && !self.check(TokenType::LBrace) {
7014            self.advance();
7015        }
7016        let store = if self.at_declaration_start()
7017            || self.check(TokenType::LBrace)
7018            || self.check(TokenType::RBrace)
7019            || self.check(TokenType::Eof)
7020        {
7021            String::new()
7022        } else {
7023            self.consume_any_ident_or_kw()?.value.clone()
7024        };
7025        let mut fields: Vec<(String, String)> = Vec::new();
7026        if self.check(TokenType::LBrace) {
7027            self.advance();
7028            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7029                let col = self.current().value.clone();
7030                self.advance();
7031                if self.check(TokenType::Colon) {
7032                    self.advance();
7033                    let value = if self.check(TokenType::StringLit) {
7034                        self.consume(TokenType::StringLit)?.value.clone()
7035                    } else if self.check(TokenType::RBrace)
7036                        || self.check(TokenType::Eof)
7037                        || self.check(TokenType::Colon)
7038                    {
7039                        String::new()
7040                    } else {
7041                        let v = self.current().clone();
7042                        self.advance();
7043                        v.value.clone()
7044                    };
7045                    fields.push((col, value));
7046                }
7047            }
7048            if self.check(TokenType::RBrace) {
7049                self.advance();
7050            }
7051        }
7052        Ok(FlowStep::Persist(PersistStep {
7053            store_name: store,
7054            fields,
7055            loc: Loc {
7056                line: tok.line,
7057                column: tok.column,
7058            },
7059        }))
7060    }
7061
7062    /// v1.30.0 — Parse a `mutate` step, capturing both the
7063    /// `{ where: "<expr>" }` filter AND the `{ col: value }` SET
7064    /// assignments.
7065    ///
7066    /// Before v1.30.0 `mutate` parsed via `parse_store_where_step`,
7067    /// which captured only `where:` and *skipped* every other key — so
7068    /// the runtime built the `UPDATE … SET` clause from every flow
7069    /// binding (params + step results + `let`s), which fails against
7070    /// any real table (`column "X" does not exist`). This closes the
7071    /// gap symmetrically to 35.o's `persist` block: every key other
7072    /// than `where:` is a SET column; a `mutate` with no SET column
7073    /// keeps the v1.31.0 user-bindings fallback. `where:` keeps its
7074    /// string-literal grammar (as in `retrieve` / `purge`).
7075    fn parse_mutate_step(&mut self) -> Result<FlowStep, ParseError> {
7076        let tok = self.current().clone();
7077        self.advance(); // consume `mutate`
7078        let store = if self.at_declaration_start()
7079            || self.check(TokenType::LBrace)
7080            || self.check(TokenType::RBrace)
7081            || self.check(TokenType::Eof)
7082        {
7083            String::new()
7084        } else {
7085            self.consume_any_ident_or_kw()?.value.clone()
7086        };
7087        let mut where_expr = String::new();
7088        let mut fields: Vec<(String, String)> = Vec::new();
7089        if self.check(TokenType::LBrace) {
7090            self.advance();
7091            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7092                let key = self.current().value.clone();
7093                self.advance();
7094                if self.check(TokenType::Colon) {
7095                    self.advance();
7096                    if key == "where" {
7097                        where_expr =
7098                            self.consume(TokenType::StringLit)?.value.clone();
7099                    } else {
7100                        let value = if self.check(TokenType::StringLit) {
7101                            self.consume(TokenType::StringLit)?.value.clone()
7102                        } else if self.check(TokenType::RBrace)
7103                            || self.check(TokenType::Eof)
7104                            || self.check(TokenType::Colon)
7105                        {
7106                            String::new()
7107                        } else {
7108                            let v = self.current().clone();
7109                            self.advance();
7110                            v.value.clone()
7111                        };
7112                        fields.push((key, value));
7113                    }
7114                }
7115            }
7116            if self.check(TokenType::RBrace) {
7117                self.advance();
7118            }
7119        }
7120        Ok(FlowStep::Mutate(MutateStep {
7121            store_name: store,
7122            where_expr,
7123            fields,
7124            loc: Loc {
7125                line: tok.line,
7126                column: tok.column,
7127            },
7128        }))
7129    }
7130
7131    // ── TIER 2 DECLARATIONS ────────────────────────────────────────
7132
7133    fn parse_agent(&mut self) -> Result<AgentDefinition, ParseError> {
7134        let tok = self.consume(TokenType::Agent)?;
7135        let name = self.consume(TokenType::Identifier)?.value;
7136        let mut node = AgentDefinition {
7137            name,
7138            goal: String::new(),
7139            tools: Vec::new(),
7140            memory_ref: String::new(),
7141            strategy: String::new(),
7142            on_stuck: String::new(),
7143            shield_ref: String::new(),
7144            max_iterations: None,
7145            max_tokens: None,
7146            max_time: String::new(),
7147            max_cost: None,
7148            return_type: String::new(),
7149            body: Vec::new(),
7150            loc: Loc {
7151                line: tok.line,
7152                column: tok.column,
7153            },
7154            leading_trivia: Vec::new(),
7155            trailing_trivia: Vec::new(),
7156        };
7157        // Optional signature position: `agent Name(params…) -> T {`. The
7158        // parameter list is accepted and not modelled (an agent takes its input
7159        // from the call site); the return type IS modelled — it used to be
7160        // skipped here, which is how `return:` became a promise the README made
7161        // and nothing read.
7162        if self.check(TokenType::LParen) {
7163            let mut depth = 0usize;
7164            while !self.check(TokenType::Eof) {
7165                if self.check(TokenType::LParen) {
7166                    depth += 1;
7167                } else if self.check(TokenType::RParen) {
7168                    depth -= 1;
7169                    if depth == 0 {
7170                        self.advance();
7171                        break;
7172                    }
7173                }
7174                self.advance();
7175            }
7176        }
7177        if self.check(TokenType::Arrow) {
7178            self.advance();
7179            node.return_type = self.parse_output_type_string()?;
7180        }
7181        self.consume(TokenType::LBrace)?;
7182        // The block is a CLOSED catalogue. An unknown field used to be skipped
7183        // in silence, so a typo (`max_iteration: 6`) parsed clean and the agent
7184        // ran unbounded until the dispatcher refused it — the opposite of what
7185        // a type error is for. Every field the runtime reads is listed here;
7186        // `step … { … }` blocks form the `custom` policy's body.
7187        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7188            if self.check(TokenType::Step) {
7189                let step = self.parse_step()?;
7190                node.body.push(step);
7191                continue;
7192            }
7193            let field = self.current().clone();
7194            let field_name = field.value.clone();
7195            self.advance();
7196            if !self.check(TokenType::Colon) {
7197                return Err(self.error(&format!(
7198                    "unexpected `{field_name}` inside `agent {}` — an agent block holds \
7199                     `field: value` pairs and `step Name {{ … }}` blocks; valid fields: \
7200                     goal, tools, memory, strategy, on_stuck, shield, max_iterations, \
7201                     max_tokens, max_time, max_cost, return",
7202                    node.name
7203                )));
7204            }
7205            self.advance();
7206            match field_name.as_str() {
7207                "goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
7208                "tools" => node.tools = self.parse_bracketed_identifiers()?,
7209                "memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
7210                "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
7211                "on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
7212                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
7213                "max_iterations" => node.max_iterations = self.parse_optional_int(),
7214                "max_tokens" => node.max_tokens = self.parse_optional_int(),
7215                "max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
7216                "max_cost" => node.max_cost = self.parse_optional_float(),
7217                "return" => node.return_type = self.parse_output_type_string()?,
7218                other => {
7219                    return Err(self.error(&format!(
7220                        "unknown agent field `{other}` in `agent {}` — the agent block is a \
7221                         closed catalog; valid fields: goal, tools, memory, strategy, \
7222                         on_stuck, shield, max_iterations, max_tokens, max_time, max_cost, \
7223                         return (plus `step Name {{ … }}` blocks for `strategy: custom`)",
7224                        node.name
7225                    )));
7226                }
7227            }
7228        }
7229        self.consume(TokenType::RBrace)?;
7230        Ok(node)
7231    }
7232
7233    /// v2.5.0 — `extension Name { category: effects|scan, members: [ … ] }`.
7234    /// The parser is permissive on field/category VALUES (validated in
7235    /// v2.5.0 by the type-checker — no-shadowing, category-membership);
7236    /// it only enforces the structural grammar here.
7237    fn parse_extension(&mut self) -> Result<ExtensionDefinition, ParseError> {
7238        let tok = self.consume(TokenType::Extension)?;
7239        let name = self.consume(TokenType::Identifier)?.value;
7240        let mut node = ExtensionDefinition {
7241            name,
7242            category: String::new(),
7243            members: Vec::new(),
7244            loc: Loc {
7245                line: tok.line,
7246                column: tok.column,
7247            },
7248            leading_trivia: Vec::new(),
7249            trailing_trivia: Vec::new(),
7250        };
7251        self.consume(TokenType::LBrace)?;
7252        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7253            let field_name = self.current().value.clone();
7254            self.advance();
7255            if self.check(TokenType::Colon) {
7256                self.advance();
7257                match field_name.as_str() {
7258                    "category" => {
7259                        node.category = self.consume_any_ident_or_kw()?.value.clone()
7260                    }
7261                    "members" => node.members = self.parse_extension_members()?,
7262                    _ => self.skip_value(),
7263                }
7264            } else if self.check(TokenType::LBrace) {
7265                self.skip_braced_block()?;
7266            }
7267        }
7268        self.consume(TokenType::RBrace)?;
7269        Ok(node)
7270    }
7271
7272    /// v2.5.0 — parse `[ "name" [: { semantics: "…", default_confidence: 0.8 } ], … ]`.
7273    /// Each member is a string literal optionally followed by a metadata
7274    /// block. Trailing/interleaved commas are tolerated.
7275    fn parse_extension_members(&mut self) -> Result<Vec<ExtensionMember>, ParseError> {
7276        let mut members = Vec::new();
7277        self.consume(TokenType::LBracket)?;
7278        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
7279            let name_tok = self.consume(TokenType::StringLit)?;
7280            let mut member = ExtensionMember {
7281                name: name_tok.value.clone(),
7282                semantics: None,
7283                default_confidence: None,
7284                loc: Loc {
7285                    line: name_tok.line,
7286                    column: name_tok.column,
7287                },
7288            };
7289            // Optional `: { semantics: "…", default_confidence: 0.8 }`.
7290            if self.check(TokenType::Colon) {
7291                self.advance();
7292                self.consume(TokenType::LBrace)?;
7293                while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7294                    let mkey = self.current().value.clone();
7295                    self.advance();
7296                    if self.check(TokenType::Colon) {
7297                        self.advance();
7298                        match mkey.as_str() {
7299                            "semantics" => {
7300                                member.semantics =
7301                                    Some(self.consume(TokenType::StringLit)?.value.clone())
7302                            }
7303                            "default_confidence" => {
7304                                member.default_confidence = self.parse_optional_float()
7305                            }
7306                            _ => self.skip_value(),
7307                        }
7308                    }
7309                    if self.check(TokenType::Comma) {
7310                        self.advance();
7311                    }
7312                }
7313                self.consume(TokenType::RBrace)?;
7314            }
7315            members.push(member);
7316            if self.check(TokenType::Comma) {
7317                self.advance();
7318            }
7319        }
7320        self.consume(TokenType::RBracket)?;
7321        Ok(members)
7322    }
7323
7324    /// v2.27.0 — `window <Name> { timezone: "…" allow: [ {days hours} ]
7325    /// exclude: [ "YYYY-MM-DD", … ]  on_outside: skip|defer|warn }`.
7326    fn parse_window(&mut self) -> Result<WindowDefinition, ParseError> {
7327        let tok = self.consume(TokenType::Window)?;
7328        let name = self.consume(TokenType::Identifier)?.value;
7329        let mut node = WindowDefinition {
7330            name,
7331            timezone: String::new(),
7332            allow: Vec::new(),
7333            exclude: Vec::new(),
7334            on_outside: String::new(),
7335            loc: Loc {
7336                line: tok.line,
7337                column: tok.column,
7338            },
7339            leading_trivia: Vec::new(),
7340            trailing_trivia: Vec::new(),
7341        };
7342        self.consume(TokenType::LBrace)?;
7343        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7344            let field_name = self.consume_any_ident_or_kw()?.value;
7345            self.consume(TokenType::Colon)?;
7346            match field_name.as_str() {
7347                "timezone" => node.timezone = self.consume(TokenType::StringLit)?.value,
7348                "allow" => node.allow = self.parse_window_allow()?,
7349                "exclude" => node.exclude = self.parse_window_exclude()?,
7350                "on_outside" => node.on_outside = self.consume_any_ident_or_kw()?.value,
7351                _ => self.skip_value(),
7352            }
7353        }
7354        self.consume(TokenType::RBrace)?;
7355        Ok(node)
7356    }
7357
7358    /// v2.27.0 — the `allow: [ { … }, { … } ]` span list.
7359    fn parse_window_allow(&mut self) -> Result<Vec<WindowSpan>, ParseError> {
7360        self.consume(TokenType::LBracket)?;
7361        let mut spans = Vec::new();
7362        if !self.check(TokenType::RBracket) {
7363            spans.push(self.parse_window_span()?);
7364            while self.check(TokenType::Comma) {
7365                self.advance();
7366                if self.check(TokenType::RBracket) {
7367                    break; // trailing comma
7368                }
7369                spans.push(self.parse_window_span()?);
7370            }
7371        }
7372        self.consume(TokenType::RBracket)?;
7373        Ok(spans)
7374    }
7375
7376    /// v2.27.0 — the `exclude: [ "YYYY-MM-DD", … ]` holiday list (ISO
7377    /// date-string literals; validated for real-calendar-date-ness by the
7378    /// `axon-T826` type check). An empty list / absent field ⇒ no holidays.
7379    fn parse_window_exclude(&mut self) -> Result<Vec<String>, ParseError> {
7380        self.consume(TokenType::LBracket)?;
7381        let mut dates = Vec::new();
7382        if !self.check(TokenType::RBracket) {
7383            dates.push(self.consume(TokenType::StringLit)?.value);
7384            while self.check(TokenType::Comma) {
7385                self.advance();
7386                if self.check(TokenType::RBracket) {
7387                    break; // trailing comma
7388                }
7389                dates.push(self.consume(TokenType::StringLit)?.value);
7390            }
7391        }
7392        self.consume(TokenType::RBracket)?;
7393        Ok(dates)
7394    }
7395
7396    /// v2.27.0 — one span `{ days: Mon..Fri hours: 9..18 }`.
7397    fn parse_window_span(&mut self) -> Result<WindowSpan, ParseError> {
7398        let tok = self.consume(TokenType::LBrace)?;
7399        let mut span = WindowSpan {
7400            day_start: String::new(),
7401            day_end: String::new(),
7402            hour_start: 0,
7403            hour_end: 0,
7404            loc: Loc {
7405                line: tok.line,
7406                column: tok.column,
7407            },
7408        };
7409        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7410            let field = self.consume_any_ident_or_kw()?.value;
7411            self.consume(TokenType::Colon)?;
7412            match field.as_str() {
7413                "days" => {
7414                    span.day_start = self.consume_any_ident_or_kw()?.value;
7415                    self.consume(TokenType::DotDot)?;
7416                    span.day_end = self.consume_any_ident_or_kw()?.value;
7417                }
7418                "hours" => {
7419                    span.hour_start = self.consume_number()? as i64;
7420                    self.consume(TokenType::DotDot)?;
7421                    span.hour_end = self.consume_number()? as i64;
7422                }
7423                _ => self.skip_value(),
7424            }
7425            if self.check(TokenType::Comma) {
7426                self.advance();
7427            }
7428        }
7429        self.consume(TokenType::RBrace)?;
7430        Ok(span)
7431    }
7432
7433    fn parse_shield(&mut self) -> Result<ShieldDefinition, ParseError> {
7434        let tok = self.consume(TokenType::Shield)?;
7435        let name = self.consume(TokenType::Identifier)?.value;
7436        let mut node = ShieldDefinition {
7437            name,
7438            scan: Vec::new(),
7439            strategy: String::new(),
7440            on_breach: String::new(),
7441            severity: String::new(),
7442            quarantine: String::new(),
7443            max_retries: None,
7444            confidence_threshold: None,
7445            allow_tools: Vec::new(),
7446            deny_tools: Vec::new(),
7447            sandbox: None,
7448            redact: Vec::new(),
7449            log: String::new(),
7450            deflect_message: String::new(),
7451            taint: String::new(),
7452            compliance: Vec::new(),
7453            sign: String::new(),
7454            unknown_fields: Vec::new(),
7455            loc: Loc {
7456                line: tok.line,
7457                column: tok.column,
7458            },
7459            leading_trivia: Vec::new(),
7460            trailing_trivia: Vec::new(),
7461        };
7462        self.consume(TokenType::LBrace)?;
7463        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7464            let field_name = self.current().value.clone();
7465            let field_loc = Loc {
7466                line: self.current().line,
7467                column: self.current().column,
7468            };
7469            self.advance();
7470            if self.check(TokenType::Colon) {
7471                self.advance();
7472                match field_name.as_str() {
7473                    "scan" => node.scan = self.parse_bracketed_identifiers()?,
7474                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
7475                    "on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
7476                    "severity" => node.severity = self.consume_any_ident_or_kw()?.value.clone(),
7477                    "quarantine" => {
7478                        node.quarantine = self.consume(TokenType::StringLit)?.value.clone()
7479                    }
7480                    "max_retries" => node.max_retries = self.parse_optional_int(),
7481                    "confidence_threshold" => {
7482                        node.confidence_threshold = self.parse_optional_float()
7483                    }
7484                    "allow_tools" => node.allow_tools = self.parse_bracketed_identifiers()?,
7485                    "deny_tools" => node.deny_tools = self.parse_bracketed_identifiers()?,
7486                    "sandbox" => {
7487                        node.sandbox = Some(self.consume_any_ident_or_kw()?.value == "true")
7488                    }
7489                    "redact" => node.redact = self.parse_bracketed_identifiers()?,
7490                    "log" => node.log = self.consume_any_ident_or_kw()?.value.clone(),
7491                    "deflect_message" => {
7492                        node.deflect_message = self.consume(TokenType::StringLit)?.value.clone()
7493                    }
7494                    "taint" => node.taint = self.consume_any_ident_or_kw()?.value.clone(),
7495                    // ESK — covered regulatory classes.
7496                    "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
7497                    // v2.34.0 — egress signing algorithm (closed catalog,
7498                    // validated by the checker: `axon-T846`).
7499                    "sign" => node.sign = self.consume_any_ident_or_kw()?.value.clone(),
7500                    // v2.34.0 — the value is still skipped (leniency
7501                    // preserved) but the NAME is recorded so the checker
7502                    // emits `axon-W010` instead of a silent drop.
7503                    _ => {
7504                        node.unknown_fields.push((field_name.clone(), field_loc));
7505                        self.skip_value()
7506                    }
7507                }
7508            } else if self.check(TokenType::LBrace) {
7509                self.skip_braced_block()?;
7510            }
7511        }
7512        self.consume(TokenType::RBrace)?;
7513        Ok(node)
7514    }
7515
7516    fn parse_pix(&mut self) -> Result<PixDefinition, ParseError> {
7517        let tok = self.consume(TokenType::Pix)?;
7518        let name = self.consume(TokenType::Identifier)?.value;
7519        let mut node = PixDefinition {
7520            name,
7521            source: String::new(),
7522            depth: None,
7523            branching: None,
7524            model: String::new(),
7525            loc: Loc {
7526                line: tok.line,
7527                column: tok.column,
7528            },
7529            leading_trivia: Vec::new(),
7530            trailing_trivia: Vec::new(),
7531        };
7532        self.consume(TokenType::LBrace)?;
7533        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7534            let field_name = self.current().value.clone();
7535            self.advance();
7536            if self.check(TokenType::Colon) {
7537                self.advance();
7538                match field_name.as_str() {
7539                    "source" => node.source = self.consume(TokenType::StringLit)?.value.clone(),
7540                    "depth" => node.depth = self.parse_optional_int(),
7541                    "branching" => node.branching = self.parse_optional_int(),
7542                    "model" => node.model = self.consume_any_ident_or_kw()?.value.clone(),
7543                    _ => self.skip_value(),
7544                }
7545            } else if self.check(TokenType::LBrace) {
7546                self.skip_braced_block()?;
7547            }
7548        }
7549        self.consume(TokenType::RBrace)?;
7550        Ok(node)
7551    }
7552
7553    /// v2.12.0 — `ledger <Name> { source, depth, branching, model }`.
7554    /// The append-only audit chain (formerly the Provenance-Index reading of
7555    /// `pix`). Field grammar mirrors `pix` (same shape) but the SEMANTICS are
7556    /// audit, not navigation: `depth` = chain retention, `branching` = Merkle
7557    /// factor, `model` = hash slug (sha256 / blake3 / sha3).
7558    fn parse_ledger(&mut self) -> Result<LedgerDefinition, ParseError> {
7559        let tok = self.consume(TokenType::Ledger)?;
7560        let name = self.consume(TokenType::Identifier)?.value;
7561        let mut node = LedgerDefinition {
7562            name,
7563            source: String::new(),
7564            depth: None,
7565            branching: None,
7566            model: String::new(),
7567            loc: Loc {
7568                line: tok.line,
7569                column: tok.column,
7570            },
7571            leading_trivia: Vec::new(),
7572            trailing_trivia: Vec::new(),
7573        };
7574        self.consume(TokenType::LBrace)?;
7575        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7576            let field_name = self.current().value.clone();
7577            self.advance();
7578            if self.check(TokenType::Colon) {
7579                self.advance();
7580                match field_name.as_str() {
7581                    "source" => node.source = self.consume(TokenType::StringLit)?.value.clone(),
7582                    "depth" => node.depth = self.parse_optional_int(),
7583                    "branching" => node.branching = self.parse_optional_int(),
7584                    "model" => node.model = self.consume_any_ident_or_kw()?.value.clone(),
7585                    _ => self.skip_value(),
7586                }
7587            } else if self.check(TokenType::LBrace) {
7588                self.skip_braced_block()?;
7589            }
7590        }
7591        self.consume(TokenType::RBrace)?;
7592        Ok(node)
7593    }
7594
7595    fn parse_psyche(&mut self) -> Result<PsycheDefinition, ParseError> {
7596        let tok = self.consume(TokenType::Psyche)?;
7597        let name = self.consume(TokenType::Identifier)?.value;
7598        let mut node = PsycheDefinition {
7599            name,
7600            dimensions: Vec::new(),
7601            manifold_noise: None,
7602            manifold_momentum: None,
7603            safety_constraints: Vec::new(),
7604            quantum_enabled: None,
7605            inference_mode: String::new(),
7606            loc: Loc {
7607                line: tok.line,
7608                column: tok.column,
7609            },
7610            leading_trivia: Vec::new(),
7611            trailing_trivia: Vec::new(),
7612        };
7613        self.consume(TokenType::LBrace)?;
7614        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7615            let field_name = self.current().value.clone();
7616            self.advance();
7617            if self.check(TokenType::Colon) {
7618                self.advance();
7619                match field_name.as_str() {
7620                    "dimensions" => node.dimensions = self.parse_bracketed_identifiers()?,
7621                    "manifold_noise" => node.manifold_noise = self.parse_optional_float(),
7622                    "manifold_momentum" => node.manifold_momentum = self.parse_optional_float(),
7623                    // v2.83.0 — `safety:` is what README psyche publishes;
7624                    // `safety_constraints:` is what the parser has always taken.
7625                    // One field, two spellings — the `epsilon`/`tolerance`
7626                    // resolution of v2.83.0.
7627                    "safety_constraints" | "safety" => {
7628                        node.safety_constraints = self.parse_bracketed_identifiers()?
7629                    }
7630                    "quantum_enabled" => {
7631                        node.quantum_enabled = Some(self.consume_any_ident_or_kw()?.value == "true")
7632                    }
7633                    "inference_mode" => {
7634                        node.inference_mode = self.consume_any_ident_or_kw()?.value.clone()
7635                    }
7636                    _ => self.skip_value(),
7637                }
7638            } else if self.check(TokenType::LBrace) {
7639                self.skip_braced_block()?;
7640            }
7641        }
7642        self.consume(TokenType::RBrace)?;
7643        Ok(node)
7644    }
7645
7646    fn parse_corpus(&mut self) -> Result<CorpusDefinition, ParseError> {
7647        let tok = self.consume(TokenType::Corpus)?;
7648        let name = self.consume(TokenType::Identifier)?.value;
7649        let mut node = CorpusDefinition {
7650            name,
7651            documents: Vec::new(),
7652            relations: Vec::new(),
7653            adaptive: false,
7654            mcp_server: String::new(),
7655            mcp_resource_uri: String::new(),
7656            store_source: None,
7657            loc: Loc {
7658                line: tok.line,
7659                column: tok.column,
7660            },
7661            leading_trivia: Vec::new(),
7662            trailing_trivia: Vec::new(),
7663        };
7664        // corpus Name from mcp("server", "uri")  — static MCP-bound short form.
7665        // corpus Name from axonstore { documents: S(id,title)  relations: … }  —
7666        // v2.14.0 dynamic store-sourced MDN graph (falls through to the body).
7667        let mut dynamic = false;
7668        if self.check(TokenType::From) {
7669            self.advance();
7670            if self.check(TokenType::AxonStore) {
7671                self.advance();
7672                dynamic = true;
7673            } else {
7674                self.consume(TokenType::Mcp)?;
7675                self.consume(TokenType::LParen)?;
7676                node.mcp_server = self.consume(TokenType::StringLit)?.value.clone();
7677                self.consume(TokenType::Comma)?;
7678                node.mcp_resource_uri = self.consume(TokenType::StringLit)?.value.clone();
7679                self.consume(TokenType::RParen)?;
7680                return Ok(node);
7681            }
7682        }
7683        self.consume(TokenType::LBrace)?;
7684        // v2.14.0 — accumulate the store-mapping pieces while the dynamic body
7685        // is parsed; folded into `node.store_source` after the closing brace.
7686        let mut src = CorpusStoreSource {
7687            doc_store: String::new(),
7688            doc_id_col: String::new(),
7689            doc_title_col: String::new(),
7690            edge_store: String::new(),
7691            edge_from_col: String::new(),
7692            edge_to_col: String::new(),
7693            edge_type_col: String::new(),
7694            edge_weight_col: String::new(),
7695            loc: Loc {
7696                line: tok.line,
7697                column: tok.column,
7698            },
7699        };
7700        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7701            let field_name = self.current().value.clone();
7702            self.advance();
7703            if self.check(TokenType::Colon) {
7704                self.advance();
7705                match field_name.as_str() {
7706                    // v2.14.0 — dynamic: `documents: <DocStore>(id_col, title_col)`.
7707                    "documents" if dynamic => {
7708                        let (store, cols) = self.parse_corpus_store_mapping(2)?;
7709                        src.doc_store = store;
7710                        src.doc_id_col = cols[0].clone();
7711                        src.doc_title_col = cols[1].clone();
7712                    }
7713                    "documents" => node.documents = self.parse_bracketed_identifiers()?,
7714                    // v2.14.0 — dynamic: `relations: <EdgeStore>(from, to, etype, weight)`.
7715                    "relations" if dynamic => {
7716                        let (store, cols) = self.parse_corpus_store_mapping(4)?;
7717                        src.edge_store = store;
7718                        src.edge_from_col = cols[0].clone();
7719                        src.edge_to_col = cols[1].clone();
7720                        src.edge_type_col = cols[2].clone();
7721                        src.edge_weight_col = cols[3].clone();
7722                    }
7723                    // v2.13.0 — static typed weighted edges → MDN corpus graph.
7724                    "relations" => node.relations = self.parse_corpus_relations()?,
7725                    // v2.13.0 — enable the memory endofunctor.
7726                    "adaptive" => node.adaptive = self.consume_any_ident_or_kw()?.value == "true",
7727                    _ => self.skip_value(),
7728                }
7729            } else if self.check(TokenType::LBrace) {
7730                self.skip_braced_block()?;
7731            }
7732        }
7733        self.consume(TokenType::RBrace)?;
7734        if dynamic {
7735            node.store_source = Some(src);
7736        }
7737        Ok(node)
7738    }
7739
7740    /// v2.14.0 — parse a store-mapping `<StoreName>(col1, col2, …)` of exactly
7741    /// `n` columns. Used by the dynamic store-sourced corpus's `documents:` (2
7742    /// cols: id, title) and `relations:` (4 cols: from, to, etype, weight). The
7743    /// store name is an identifier (a declared `axonstore`); the columns may be
7744    /// keywords (a column could be named `from`/`type`), so they use the
7745    /// keyword-tolerant consumer. The type-checker validates store + columns.
7746    fn parse_corpus_store_mapping(&mut self, n: usize) -> Result<(String, Vec<String>), ParseError> {
7747        let store = self.consume(TokenType::Identifier)?.value.clone();
7748        self.consume(TokenType::LParen)?;
7749        let mut cols = Vec::with_capacity(n);
7750        for i in 0..n {
7751            if i > 0 {
7752                self.consume(TokenType::Comma)?;
7753            }
7754            cols.push(self.consume_any_ident_or_kw()?.value.clone());
7755        }
7756        self.consume(TokenType::RParen)?;
7757        Ok((store, cols))
7758    }
7759
7760    /// v2.13.0 — parse `relations: [ etype(from, to, weight) … ]`, the typed
7761    /// weighted edges of an MDN corpus graph. Entries are whitespace/newline
7762    /// separated; commas between them are optional. Edge-type validity (closed
7763    /// catalog), document references, and the weight range are checked by the
7764    /// type-checker (`check_corpus`), not here.
7765    fn parse_corpus_relations(&mut self) -> Result<Vec<CorpusRelation>, ParseError> {
7766        let mut out = Vec::new();
7767        self.consume(TokenType::LBracket)?;
7768        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
7769            if self.check(TokenType::Comma) {
7770                self.advance();
7771                continue;
7772            }
7773            let tok = self.current().clone();
7774            let etype = self.consume_any_ident_or_kw()?.value.clone();
7775            self.consume(TokenType::LParen)?;
7776            let from = self.consume_any_ident_or_kw()?.value.clone();
7777            self.consume(TokenType::Comma)?;
7778            let to = self.consume_any_ident_or_kw()?.value.clone();
7779            self.consume(TokenType::Comma)?;
7780            let weight = self.consume_number()?;
7781            self.consume(TokenType::RParen)?;
7782            out.push(CorpusRelation {
7783                etype,
7784                from,
7785                to,
7786                weight,
7787                loc: Loc { line: tok.line, column: tok.column },
7788            });
7789        }
7790        self.consume(TokenType::RBracket)?;
7791        Ok(out)
7792    }
7793
7794    /// v2.63.0 — the typed dataspace declaration:
7795    ///
7796    /// ```text
7797    /// dataspace <Name> {
7798    ///     column <name>: <Type>
7799    ///     …
7800    /// }
7801    /// ```
7802    ///
7803    /// Until 108.b the body was consumed by `skip_braced_block()` — any
7804    /// content, including garbage, compiled clean and reached nothing.
7805    /// Now each entry must be a `column` field; the declared type is
7806    /// kept RAW here and resolved against the closed 6-type catalog by
7807    /// the type-checker (`axon-T928`), so all schema errors accumulate
7808    /// in a single compile. An unknown body keyword is a parse error
7809    /// (the grammar is closed — the v1.31.0 axonstore posture).
7810    fn parse_dataspace(&mut self) -> Result<DataspaceDefinition, ParseError> {
7811        let tok = self.consume(TokenType::Dataspace)?;
7812        let name = self.consume(TokenType::Identifier)?.value;
7813        let mut node = DataspaceDefinition {
7814            name,
7815            columns: Vec::new(),
7816            loc: Loc {
7817                line: tok.line,
7818                column: tok.column,
7819            },
7820            leading_trivia: Vec::new(),
7821            trailing_trivia: Vec::new(),
7822        };
7823        if self.check(TokenType::LBrace) {
7824            self.consume(TokenType::LBrace)?;
7825            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7826                let entry = self.current().clone();
7827                if entry.value != "column" {
7828                    return Err(ParseError {
7829                        message: format!(
7830                            "Unknown entry `{}` in dataspace `{}`. A dataspace body \
7831                             declares its columnar schema: `column <name>: <Type>` \
7832                             (one per line, over the closed type catalog — \
7833                             Text, Int, Float, Bool, Timestamp, Json).",
7834                            entry.value, node.name
7835                        ),
7836                        line: entry.line,
7837                        column: entry.column,
7838                        ..Default::default()
7839                    });
7840                }
7841                self.advance(); // `column`
7842                let col_tok = self.current().clone();
7843                let col_name = self.consume_any_ident_or_kw()?.value.clone();
7844                self.consume(TokenType::Colon)?;
7845                let declared_type = self.consume_any_ident_or_kw()?.value.clone();
7846                node.columns.push(crate::ast::DataspaceColumn {
7847                    name: col_name,
7848                    declared_type,
7849                    loc: Loc {
7850                        line: col_tok.line,
7851                        column: col_tok.column,
7852                    },
7853                });
7854            }
7855            self.consume(TokenType::RBrace)?;
7856        }
7857        Ok(node)
7858    }
7859
7860    fn parse_ots(&mut self) -> Result<OtsDefinition, ParseError> {
7861        let tok = self.consume(TokenType::Ots)?;
7862        let name = self.consume(TokenType::Identifier)?.value;
7863        let mut node = OtsDefinition {
7864            name,
7865            teleology: String::new(),
7866            homotopy_search: String::new(),
7867            loss_function: String::new(),
7868            loc: Loc {
7869                line: tok.line,
7870                column: tok.column,
7871            },
7872            leading_trivia: Vec::new(),
7873            trailing_trivia: Vec::new(),
7874        };
7875        // Skip optional type params <In, Out>
7876        if self.check(TokenType::Lt) {
7877            while !self.check(TokenType::Gt) && !self.check(TokenType::Eof) {
7878                self.advance();
7879            }
7880            if self.check(TokenType::Gt) {
7881                self.advance();
7882            }
7883        }
7884        self.consume(TokenType::LBrace)?;
7885        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7886            let field_name = self.current().value.clone();
7887            self.advance();
7888            if self.check(TokenType::Colon) {
7889                self.advance();
7890                match field_name.as_str() {
7891                    "teleology" => {
7892                        node.teleology = self.consume(TokenType::StringLit)?.value.clone()
7893                    }
7894                    "homotopy_search" => {
7895                        node.homotopy_search = self.consume_any_ident_or_kw()?.value.clone()
7896                    }
7897                    // v2.83.0 — README's ots blocks write the loss as a bare
7898                    // identifier (`loss_function: SemanticPreservation`, `L2`,
7899                    // `Contrastive`); the parser accepted only a string literal, so
7900                    // all three published blocks failed at this exact token. Both
7901                    // spellings resolve to the same field.
7902                    "loss_function" => {
7903                        node.loss_function = if self.check(TokenType::StringLit) {
7904                            self.consume(TokenType::StringLit)?.value.clone()
7905                        } else {
7906                            self.consume_any_ident_or_kw()?.value.clone()
7907                        }
7908                    }
7909                    _ => self.skip_value(),
7910                }
7911            } else if self.check(TokenType::LBrace) {
7912                self.skip_braced_block()?;
7913            }
7914        }
7915        self.consume(TokenType::RBrace)?;
7916        Ok(node)
7917    }
7918
7919    fn parse_mandate(&mut self) -> Result<MandateDefinition, ParseError> {
7920        let tok = self.consume(TokenType::Mandate)?;
7921        let name = self.consume(TokenType::Identifier)?.value;
7922        let mut node = MandateDefinition {
7923            name,
7924            constraint: String::new(),
7925            kp: None,
7926            ki: None,
7927            kd: None,
7928            tolerance: None,
7929            max_steps: None,
7930            drift_bound: None,
7931            lipschitz: None,
7932            on_violation: String::new(),
7933            loc: Loc {
7934                line: tok.line,
7935                column: tok.column,
7936            },
7937            leading_trivia: Vec::new(),
7938            trailing_trivia: Vec::new(),
7939        };
7940        self.consume(TokenType::LBrace)?;
7941        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7942            let field_name = self.current().value.clone();
7943            self.advance();
7944            if self.check(TokenType::Colon) {
7945                self.advance();
7946                match field_name.as_str() {
7947                    "constraint" => {
7948                        node.constraint = self.consume(TokenType::StringLit)?.value.clone()
7949                    }
7950                    "kp" | "Kp" => node.kp = self.parse_optional_float(),
7951                    "ki" | "Ki" => node.ki = self.parse_optional_float(),
7952                    "kd" | "Kd" => node.kd = self.parse_optional_float(),
7953                    "max_steps" => node.max_steps = self.parse_optional_int(),
7954                    // v2.83.0 — `epsilon:` is what the README publishes; `tolerance:`
7955                    // is what the parser has always accepted. They are the SAME ε — the
7956                    // convergence band of `Converge(e, ε, N)`. Both spellings resolve here
7957                    // rather than one of them silently vanishing into `skip_value()`.
7958                    "tolerance" | "epsilon" => node.tolerance = self.parse_optional_float(),
7959                    "on_violation" => {
7960                        node.on_violation = self.consume_any_ident_or_kw()?.value.clone()
7961                    }
7962                    _ => self.skip_value(),
7963                }
7964            } else if self.check(TokenType::LBrace) {
7965                // v2.83.0 — `pid { Kp: 2.0, Ki: 0.3, Kd: 0.1 }`, which is the form
7966                // README XV publishes and the form every mandate example uses.
7967                //
7968                // THIS BLOCK USED TO BE `skip_braced_block()`. The consequence was not a
7969                // parse error — it was SILENT ACCEPTANCE: `axon check` printed
7970                // "0 errors" and the IR came out with `kp: None, ki: None, kd: None`.
7971                // The developer wrote the published example, the compiler agreed, and the
7972                // ENTIRE CONTROL LAW was discarded between them. A dropped specification
7973                // that reports success is the v2.67.0 defect living in the parser.
7974                if field_name == "pid" {
7975                    self.parse_pid_block(&mut node)?;
7976                } else if field_name == "stability" {
7977                    self.parse_stability_block(&mut node)?;
7978                } else {
7979                    self.skip_braced_block()?;
7980                }
7981            }
7982        }
7983        self.consume(TokenType::RBrace)?;
7984        Ok(node)
7985    }
7986
7987    /// v2.83.0 — `pid { Kp: <f>, Ki: <f>, Kd: <f> }`.
7988    ///
7989    /// The gains of the Cybernetic Refinement Calculus controller
7990    /// (`papers/paper_mandate.md` section 3): `u(t) = Kp·e(t) + Ki·∫e + Kd·de/dt`.
7991    /// Accepts both capitalised (`Kp`, the papers' and README's notation) and
7992    /// lower-case spellings, because the flat `kp:` form was already accepted and
7993    /// removing it would break programs that use it.
7994    ///
7995    /// v2.83.0 — unknown keys inside the block are REFUSED.
7996    ///
7997    /// v2.83.0 left them skipped, reasoning that the enclosing declaration behaves
7998    /// that way and tightening it was a wider decision. Measuring the published
7999    /// 2.84.0 binary showed what that costs, and the cost is not symmetric:
8000    /// misspelling a GAIN is caught (the missing gain fails the sign conditions),
8001    /// but misspelling a BOUND is not — `stability { drift: 0.5, L: 0.25 }`
8002    /// compiles clean, and the mandate is admitted with no Lyapunov floor at all.
8003    /// The typo does not weaken the check, it DELETES it.
8004    ///
8005    /// These two blocks are not like the enclosing declaration. They are closed
8006    /// catalogues of three and two keys, every one of which is a proof obligation,
8007    /// and an unrecognised key here is never a field a later version will use —
8008    /// it is a typo whose price is a silently discharged safety property.
8009    fn parse_pid_block(&mut self, node: &mut MandateDefinition) -> Result<(), ParseError> {
8010        self.consume(TokenType::LBrace)?;
8011        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8012            let key_token = self.current().clone();
8013            let key = key_token.value.clone();
8014            self.advance();
8015            if self.check(TokenType::Colon) {
8016                self.advance();
8017                match key.as_str() {
8018                    "kp" | "Kp" => node.kp = self.parse_optional_float(),
8019                    "ki" | "Ki" => node.ki = self.parse_optional_float(),
8020                    "kd" | "Kd" => node.kd = self.parse_optional_float(),
8021                    _ => {
8022                        return Err(ParseError {
8023                            message: format!(
8024                                "`{key}` is not a gain of the PID controller. The block accepts \
8025                                 exactly `Kp`, `Ki` and `Kd` (lower-case spellings too). \
8026                                 Skipping what it does not recognise would let a typo drop a \
8027                                 gain, and the stability band is computed from all three."
8028                            ),
8029                            line: key_token.line,
8030                            column: key_token.column,
8031                            ..Default::default()
8032                        });
8033                    }
8034                }
8035            }
8036            if self.check(TokenType::Comma) {
8037                self.advance();
8038            }
8039        }
8040        self.consume(TokenType::RBrace)?;
8041        Ok(())
8042    }
8043
8044    /// v2.83.0 — `stability { D: <f>, L: <f> }`.
8045    ///
8046    /// The declared hypotheses of the mandate's stability theorem: `D` is the
8047    /// drift bound `sup|drift(t)|` (paper_mandate section 3), `L` the Lipschitz
8048    /// constant of the refinement map (prompt_opt section 6.3). With them declared,
8049    /// the type checker verifies the full band `D < |Kp+Ki+Kd| < 1/L`; without
8050    /// them it can verify only the sign conditions, which the papers show to be
8051    /// necessary but not sufficient. The declaration travels in the IR as a
8052    /// proof obligation for dispatch — the compiler never invents these
8053    /// numbers, because they are measured properties of a backend it cannot
8054    /// see, and fabricating them would make the static check vacuous.
8055    ///
8056    /// An empty block is a PARSE error, not a silent no-op: `stability { }`
8057    /// asserts nothing, can discharge nothing, and the developer who wrote it
8058    /// believed otherwise.
8059    fn parse_stability_block(
8060        &mut self,
8061        node: &mut MandateDefinition,
8062    ) -> Result<(), ParseError> {
8063        let open = self.consume(TokenType::LBrace)?;
8064        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8065            let key_token = self.current().clone();
8066            let key = key_token.value.clone();
8067            self.advance();
8068            if self.check(TokenType::Colon) {
8069                self.advance();
8070                match key.as_str() {
8071                    "D" | "d" | "drift_bound" => {
8072                        node.drift_bound = self.parse_optional_float()
8073                    }
8074                    "L" | "l" | "lipschitz" => node.lipschitz = self.parse_optional_float(),
8075                    // v2.83.0 — see `parse_pid_block`. This is the arm that
8076                    // was actually dangerous: a dropped bound is a dropped
8077                    // hypothesis, and the theorem it guards then holds vacuously.
8078                    _ => {
8079                        return Err(ParseError {
8080                            message: format!(
8081                                "`{key}` is not a hypothesis of the stability theorem. The block \
8082                                 accepts exactly `D` (the drift bound, also spelled `d` or \
8083                                 `drift_bound`) and `L` (the Lipschitz constant, also `l` or \
8084                                 `lipschitz`). This is an error rather than a skipped key \
8085                                 because a bound that fails to parse is a bound that is not \
8086                                 declared, and the compiler would then verify the band it can \
8087                                 see — the sign conditions — and admit the mandate as if the \
8088                                 rest had been checked."
8089                            ),
8090                            line: key_token.line,
8091                            column: key_token.column,
8092                            ..Default::default()
8093                        });
8094                    }
8095                }
8096            }
8097            if self.check(TokenType::Comma) {
8098                self.advance();
8099            }
8100        }
8101        self.consume(TokenType::RBrace)?;
8102        if node.drift_bound.is_none() && node.lipschitz.is_none() {
8103            return Err(ParseError {
8104                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."
8105                    .to_string(),
8106                line: open.line,
8107                column: open.column,
8108                ..Default::default()
8109            });
8110        }
8111        Ok(())
8112    }
8113
8114    /// v2.67.0 — `compute <Name>(p: T, …) -> T { <expr> }`.
8115    ///
8116    /// # What this used to be
8117    ///
8118    /// ```text
8119    /// // Skip optional parameters/return type before brace
8120    /// while !self.check(TokenType::LBrace) { self.advance(); }
8121    /// ```
8122    ///
8123    /// The parameters and the return type were **skipped token by token**, and
8124    /// the brace held only `shield:`. So a `compute` had **no inputs, no output
8125    /// type and no body** — which is why the runtime could do nothing but bind
8126    /// the literal string `"compute:Name(args)"`, and why a downstream step then
8127    /// consumed that text where it expected a number. The README meanwhile
8128    /// promised "native Fast-Path execution bypassing the LLM" **with an O(n)
8129    /// guarantee**.
8130    ///
8131    /// # What it is now
8132    ///
8133    /// A named pure function over the v2.26.0 expression language — the closed,
8134    /// total, side-effect-free term algebra the runtime already evaluates
8135    /// natively (`eval_expr`, the same evaluator behind `let`, `grad` and
8136    /// `conditional`). Linear in the term, no model in the loop: the advertised
8137    /// claim, made true rather than louder.
8138    ///
8139    /// The legacy field form (`compute N { shield: G }`) still parses — its body
8140    /// is simply `None`, and applying a bodyless compute is refused (axon-T941)
8141    /// instead of silently binding a placeholder.
8142    fn parse_compute(&mut self) -> Result<ComputeDefinition, ParseError> {
8143        let tok = self.consume(TokenType::Compute)?;
8144        let name = self.consume(TokenType::Identifier)?.value;
8145        let mut node = ComputeDefinition {
8146            name,
8147            shield_ref: String::new(),
8148            parameters: Vec::new(),
8149            return_type: String::new(),
8150            body: None,
8151            loc: Loc {
8152                line: tok.line,
8153                column: tok.column,
8154            },
8155            leading_trivia: Vec::new(),
8156            trailing_trivia: Vec::new(),
8157        };
8158
8159        // `(p: T, q: T)` — the typed parameters (they used to be skipped).
8160        if self.check(TokenType::LParen) {
8161            self.advance();
8162            while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
8163                let ptok = self.current().clone();
8164                let pname = self.consume_any_ident_or_kw()?.value.clone();
8165                self.consume(TokenType::Colon)?;
8166                let ptype = self.parse_type_expr()?;
8167                node.parameters.push(Parameter {
8168                    name: pname,
8169                    type_expr: ptype,
8170                    loc: self.loc_of(&ptok),
8171                });
8172                if self.check(TokenType::Comma) {
8173                    self.advance();
8174                }
8175            }
8176            self.consume(TokenType::RParen)?;
8177        }
8178
8179        // `-> T` — the declared result type.
8180        if self.check(TokenType::Arrow) {
8181            self.advance();
8182            node.return_type = self.consume_any_ident_or_kw()?.value.clone();
8183        }
8184
8185        self.consume(TokenType::LBrace)?;
8186        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8187            // A `<name>:` pair is a legacy field (only `shield:` is meaningful).
8188            // Anything else is THE BODY — a v2.26.0 expression.
8189            //
8190            // NOTE: the field name may be a KEYWORD, not just an identifier —
8191            // `shield` is `TokenType::Shield`. Testing only for `Identifier` here
8192            // sent `compute N { shield: G }` (the legacy declaration form, and
8193            // the shape of the shipped canonical program) down the
8194            // expression-parsing path and broke it. Back-compat is not optional:
8195            // an adopter's existing program must keep compiling.
8196            let is_field = self
8197                .tokens
8198                .get(self.pos + 1)
8199                .map(|t| t.ttype == TokenType::Colon)
8200                .unwrap_or(false);
8201            if is_field {
8202                let field_tok = self.current().clone();
8203                let field_name = self.current().value.clone();
8204                self.advance();
8205                self.consume(TokenType::Colon)?;
8206                match field_name.as_str() {
8207                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
8208                    // v2.83.0 — `input: a (Float), b (Float)`.
8209                    //
8210                    // This is the parameter list EVERY published compute writes,
8211                    // and it was reaching `skip_value()` — silently discarded, so
8212                    // a compute declared this way had no parameters at all and
8213                    // `run_compute_apply` refused it on arity. The typed form
8214                    // `(a: Float, b: Float)` above stays accepted; both fill the
8215                    // same `parameters`, because they are one concept spelled two
8216                    // ways and a second slot would let them disagree.
8217                    "input" => self.parse_compute_input_list(&mut node)?,
8218                    // v2.83.0 — `output: Float` / `output: PremiumResult`,
8219                    // the field spelling of `-> T`.
8220                    "output" => {
8221                        node.return_type = self.parse_output_type_string()?;
8222                    }
8223                    _ => self.skip_value(),
8224                }
8225                let _ = field_tok;
8226            } else if self.current().value == "logic"
8227                && self
8228                    .tokens
8229                    .get(self.pos + 1)
8230                    .is_some_and(|t| t.ttype == TokenType::LBrace)
8231            {
8232                // v2.83.0 — `logic { let … return … }`, the body form all
8233                // four published computes write. It used to fall to
8234                // `parse_expr()`, which met the bare word `logic` and produced a
8235                // diagnostic about an expression the author never wrote.
8236                if node.body.is_some() {
8237                    return Err(ParseError {
8238                        message: "compute declares two bodies; a pure function has one result, \
8239                                  and keeping the last silently would discard the first"
8240                            .to_string(),
8241                        line: self.current().line,
8242                        column: self.current().column,
8243                        ..Default::default()
8244                    });
8245                }
8246                node.body = Some(self.parse_logic_block()?);
8247            } else {
8248                node.body = Some(self.parse_expr()?);
8249            }
8250        }
8251        self.consume(TokenType::RBrace)?;
8252        Ok(node)
8253    }
8254
8255    /// v2.83.0 — `input: base_rate (Float), risk_factor (Float)`.
8256    ///
8257    /// The published spelling inverts the typed form's punctuation: the name
8258    /// comes first and the type rides in parentheses. Both land in
8259    /// `ComputeDefinition::parameters`.
8260    fn parse_compute_input_list(&mut self, node: &mut ComputeDefinition) -> Result<(), ParseError> {
8261        loop {
8262            let ptok = self.current().clone();
8263            let pname = self.consume_any_ident_or_kw()?.value.clone();
8264            // The type is optional in principle; every published compute writes
8265            // it, and a parameter with no declared type cannot be checked, so an
8266            // absent one is recorded as empty rather than invented.
8267            let type_expr = if self.check(TokenType::LParen) {
8268                self.advance();
8269                let t = self.parse_type_expr()?;
8270                self.consume(TokenType::RParen)?;
8271                t
8272            } else {
8273                TypeExpr {
8274                    name: String::new(),
8275                    generic_param: String::new(),
8276                    optional: false,
8277                    loc: self.loc_of(&ptok),
8278                }
8279            };
8280            node.parameters.push(Parameter {
8281                name: pname,
8282                type_expr,
8283                loc: self.loc_of(&ptok),
8284            });
8285            if self.check(TokenType::Comma) {
8286                self.advance();
8287            } else {
8288                break;
8289            }
8290        }
8291        Ok(())
8292    }
8293
8294    /// v2.83.0 — the `logic { }` body: a chain of `let`s closed by `return`.
8295    ///
8296    /// Lowered to nested [`Expr::Let`] terms, innermost-last, so
8297    /// `let a = e₁  let b = e₂  return e₃` becomes `Let(a, e₁, Let(b, e₂, e₃))`.
8298    /// That is one evaluation per binding — substituting the bindings into the
8299    /// return expression instead would re-evaluate every bound term once per
8300    /// mention.
8301    ///
8302    /// `return` is REQUIRED. A `logic` block whose last statement is a `let`
8303    /// binds names and produces nothing; the compute would then have to invent a
8304    /// result, and inventing the result of a deterministic function is the one
8305    /// thing this primitive exists not to do.
8306    fn parse_logic_block(&mut self) -> Result<Expr, ParseError> {
8307        let open = self.current().clone();
8308        self.advance(); // `logic`
8309        self.consume(TokenType::LBrace)?;
8310
8311        let mut bindings: Vec<(String, Expr)> = Vec::new();
8312        let mut result: Option<Expr> = None;
8313
8314        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8315            if self.check(TokenType::Let) {
8316                if result.is_some() {
8317                    return Err(ParseError {
8318                        message: "a `let` after the `return` in a `logic { }` block is \
8319                                  unreachable — the block's value is already decided. Move it \
8320                                  above the `return`."
8321                            .to_string(),
8322                        line: self.current().line,
8323                        column: self.current().column,
8324                        ..Default::default()
8325                    });
8326                }
8327                self.advance(); // `let`
8328                let name = self.consume_any_ident_or_kw()?.value.clone();
8329                self.consume(TokenType::Assign)?;
8330                bindings.push((name, self.parse_expr()?));
8331            } else if self.check(TokenType::Return) {
8332                self.advance();
8333                result = Some(self.parse_expr()?);
8334            } else {
8335                let bad = self.current().clone();
8336                return Err(ParseError {
8337                    message: format!(
8338                        "unexpected `{}` in a `logic {{ }}` block — it admits only `let <name> = \
8339                         <expr>` bindings and a closing `return <expr>`. `compute` is a PURE \
8340                         function (its own paper: \"pureza categórica de los morfismos \
8341                         funcionales\"), so a statement that could have an effect is refused \
8342                         rather than parsed and dropped.",
8343                        bad.value
8344                    ),
8345                    line: bad.line,
8346                    column: bad.column,
8347                    ..Default::default()
8348                });
8349            }
8350        }
8351        self.consume(TokenType::RBrace)?;
8352
8353        let mut expr = result.ok_or_else(|| ParseError {
8354            message: "a `logic { }` block must end in `return <expr>`. Without it the block binds \
8355                      names and yields nothing, and the compute would have to invent a result — \
8356                      which is precisely what a deterministic primitive must never do."
8357                .to_string(),
8358            line: open.line,
8359            column: open.column,
8360            ..Default::default()
8361        })?;
8362
8363        // Fold innermost-last so the first `let` written is the outermost scope.
8364        for (name, value) in bindings.into_iter().rev() {
8365            expr = Expr::Let {
8366                name,
8367                value: Box::new(value),
8368                body: Box::new(expr),
8369            };
8370        }
8371        Ok(expr)
8372    }
8373
8374    fn parse_daemon(&mut self) -> Result<DaemonDefinition, ParseError> {
8375        let tok = self.consume(TokenType::Daemon)?;
8376        let name = self.consume(TokenType::Identifier)?.value;
8377        let mut node = DaemonDefinition {
8378            name,
8379            goal: String::new(),
8380            tools: Vec::new(),
8381            memory_ref: String::new(),
8382            strategy: String::new(),
8383            on_stuck: String::new(),
8384            shield_ref: String::new(),
8385            window_ref: String::new(),
8386            budget: None,
8387            max_tokens: None,
8388            max_time: String::new(),
8389            max_cost: None,
8390            listeners: Vec::new(),
8391            requires_capabilities: Vec::new(),
8392            loc: Loc {
8393                line: tok.line,
8394                column: tok.column,
8395            },
8396            leading_trivia: Vec::new(),
8397            trailing_trivia: Vec::new(),
8398        };
8399        // Skip optional parameters/return type before brace
8400        while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
8401            self.advance();
8402        }
8403        self.consume(TokenType::LBrace)?;
8404        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8405            let field = self.current().clone();
8406            let field_name = field.value.clone();
8407            self.advance();
8408            if self.check(TokenType::Colon) {
8409                self.advance();
8410                match field_name.as_str() {
8411                    "goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
8412                    "tools" => node.tools = self.parse_bracketed_identifiers()?,
8413                    "memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
8414                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
8415                    "on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
8416                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
8417                    // v2.27.0 — `window: <WindowName>` temporal binding.
8418                    "window" => node.window_ref = self.consume_any_ident_or_kw()?.value.clone(),
8419                    "max_tokens" => node.max_tokens = self.parse_optional_int(),
8420                    "max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
8421                    "max_cost" => node.max_cost = self.parse_optional_float(),
8422                    // v2.4.0 — `requires: [cap, …]` capability scope (same
8423                    // closed slug grammar as `axonendpoint requires:`). The
8424                    // enterprise supervisor mints a per-run principal scoped to
8425                    // exactly these (least privilege).
8426                    "requires" => {
8427                        let bracket_tok = self.current().clone();
8428                        let items = self.parse_bracketed_dot_identifiers()?;
8429                        for slug in &items {
8430                            if !is_valid_capability_slug(slug) {
8431                                return Err(ParseError {
8432                                    message: format!(
8433                                        "Invalid capability slug '{slug}' in daemon '{}' \
8434                                         `requires:`. Capability slugs must match \
8435                                         ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
8436                                         lowercase identifiers. Examples: `daemon.run`, \
8437                                         `memory.write`, `flow.execute`.",
8438                                        node.name
8439                                    ),
8440                                    line: bracket_tok.line,
8441                                    column: bracket_tok.column,
8442                                    ..Default::default()
8443                                });
8444                            }
8445                        }
8446                        node.requires_capabilities = items;
8447                    }
8448                    _ => self.skip_value(),
8449                }
8450            } else if field.ttype == TokenType::Listen {
8451                // v1.6.0 D4 — preserve listen blocks for type
8452                // checking.  We backtracked past the `listen` keyword
8453                // by `advance()` above, so reconstruct a synthetic
8454                // listener using the same dual-mode dispatch the flow
8455                // step parser uses (string topic OR typed channel ref).
8456                let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
8457                    (self.consume(TokenType::StringLit)?.value.clone(), false)
8458                } else {
8459                    (self.consume_any_ident_or_kw()?.value.clone(), true)
8460                };
8461                let mut alias = String::new();
8462                if !self.at_declaration_start()
8463                    && !self.check(TokenType::RBrace)
8464                    && !self.check(TokenType::LBrace)
8465                {
8466                    let next = self.current().clone();
8467                    if next.value == "as" || next.ttype == TokenType::As {
8468                        self.advance();
8469                        alias = self.consume_any_ident_or_kw()?.value.clone();
8470                    }
8471                }
8472                let listen_loc = Loc {
8473                    line: field.line,
8474                    column: field.column,
8475                };
8476                // v2.4.0 — parse the handler body (was skipped). This is
8477                // what makes a `daemon` operational: the body runs per event /
8478                // scheduled tick (e.g. a `listen "cron:…" as tick { run … }`).
8479                let body = self.parse_listener_body()?;
8480                node.listeners.push(ListenStep {
8481                    channel,
8482                    channel_is_ref,
8483                    event_alias: alias,
8484                    body,
8485                    loc: listen_loc,
8486                });
8487            } else if field_name == "budget" && self.check(TokenType::LBrace) {
8488                // v2.28.0 — the `budget { … }` linear-effect rate-limit block.
8489                node.budget = Some(self.parse_budget_block(field.line, field.column)?);
8490            } else if self.check(TokenType::LBrace) {
8491                self.skip_braced_block()?;
8492            }
8493        }
8494        self.consume(TokenType::RBrace)?;
8495        Ok(node)
8496    }
8497
8498    /// v2.69.0 — a TOP-LEVEL `budget <Name> { … }`.
8499    ///
8500    /// Same body as the daemon-attached block; what it gains is a **name** and a
8501    /// **scope that is not a daemon**. Until v2.69.0, `budget` was a field of `daemon`
8502    /// and of nothing else — so an adopter deploying an HTTP endpoint that calls a
8503    /// vendor tool had **no way in the language to bound how often it did that.**
8504    /// Not "the bound did not work": **the bound could not be written.** And the
8505    /// HTTP endpoint is what people actually deploy.
8506    fn parse_top_level_budget(&mut self) -> Result<BudgetBlock, ParseError> {
8507        let kw = self.consume(TokenType::Budget)?; // `budget`
8508        let name = self.consume(TokenType::Identifier)?.value;
8509        let mut block = self.parse_budget_block(kw.line, kw.column)?;
8510        block.name = name;
8511        Ok(block)
8512    }
8513
8514    /// v2.28.0 — `budget { <rate|max>: N per <period> on Tool(<X>) … [on_exhausted: <p>] }`.
8515    fn parse_budget_block(&mut self, line: u32, column: u32) -> Result<BudgetBlock, ParseError> {
8516        self.consume(TokenType::LBrace)?;
8517        let mut quotas = Vec::new();
8518        let mut on_exhausted = String::new();
8519        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8520            let field = self.current().clone();
8521            let field_name = self.consume_any_ident_or_kw()?.value;
8522            match field_name.as_str() {
8523                "rate" | "max" => {
8524                    quotas.push(self.parse_budget_quota(field_name, field.line, field.column)?);
8525                }
8526                "on_exhausted" => {
8527                    self.consume(TokenType::Colon)?;
8528                    on_exhausted = self.consume_any_ident_or_kw()?.value;
8529                }
8530                _ => self.skip_value(),
8531            }
8532        }
8533        self.consume(TokenType::RBrace)?;
8534        Ok(BudgetBlock {
8535            name: String::new(),
8536            quotas,
8537            on_exhausted,
8538            loc: Loc { line, column },
8539            leading_trivia: Vec::new(),
8540            trailing_trivia: Vec::new(),
8541        })
8542    }
8543
8544    /// v2.28.0 — one quota line: `<kind>: <limit> per <period> on Tool(<effect>)`.
8545    /// `kind` (`rate`/`max`) is already consumed by the caller.
8546    fn parse_budget_quota(
8547        &mut self,
8548        kind: String,
8549        line: u32,
8550        column: u32,
8551    ) -> Result<BudgetQuota, ParseError> {
8552        self.consume(TokenType::Colon)?;
8553        let limit = self.consume_number()? as i64;
8554        // `per <period>`
8555        let _per = self.consume_any_ident_or_kw()?; // the `per` keyword
8556        let period = self.consume_any_ident_or_kw()?.value;
8557        // `on Tool(<effect>)`
8558        let _on = self.consume_any_ident_or_kw()?; // the `on` keyword
8559        let _tool = self.consume_any_ident_or_kw()?; // the `Tool` wrapper keyword
8560        self.consume(TokenType::LParen)?;
8561        let effect = self.consume_any_ident_or_kw()?.value;
8562        self.consume(TokenType::RParen)?;
8563        Ok(BudgetQuota {
8564            kind,
8565            limit,
8566            period,
8567            effect,
8568            loc: Loc { line, column },
8569        })
8570    }
8571
8572    fn parse_axonstore(&mut self) -> Result<AxonStoreDefinition, ParseError> {
8573        let tok = self.consume(TokenType::AxonStore)?;
8574        let name = self.consume(TokenType::Identifier)?.value;
8575        let mut node = AxonStoreDefinition {
8576            name,
8577            backend: String::new(),
8578            connection: String::new(),
8579            resource_ref: String::new(),
8580            confidence_floor: None,
8581            isolation: String::new(),
8582            on_breach: String::new(),
8583            capability: String::new(),
8584            class: String::new(),
8585            column_schema: None,
8586            loc: Loc {
8587                line: tok.line,
8588                column: tok.column,
8589            },
8590            leading_trivia: Vec::new(),
8591            trailing_trivia: Vec::new(),
8592        };
8593        self.consume(TokenType::LBrace)?;
8594        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8595            let field = self.current().clone();
8596            let field_name = field.value.clone();
8597            // v1.31.0 (D1) — `schema:` declaration in three closed
8598            // forms: inline column block, manifest reference (string
8599            // literal), or env-var schema namespace (`env:VAR` —
8600            // unquoted or quoted). Parse the form; the v1.31.0 / v1.31.0
8601            // type-checker consumes the resulting AST.
8602            if field.ttype == TokenType::Schema {
8603                self.advance();
8604                let parsed = self.parse_store_schema_declaration(&node.name, field.line, field.column)?;
8605                node.column_schema = Some(parsed);
8606                continue;
8607            }
8608            self.advance();
8609            if self.check(TokenType::Colon) {
8610                self.advance();
8611                match field_name.as_str() {
8612                    "backend" => node.backend = self.consume_any_ident_or_kw()?.value.clone(),
8613                    // v2.48.0 — the secret-class prefix of a
8614                    // `backend: secrets` metadata store. Dotted-identifier
8615                    // form (`class: crm`, `class: crm.oauth`); the
8616                    // secrets-only placement rule + slug shape are
8617                    // `axon-T900` in the type-checker (it needs the
8618                    // resolved `backend:`, which may appear after this
8619                    // field in source order).
8620                    "class" => node.class = self.parse_dotted_identifier()?,
8621                    "connection" => node.connection = self.parse_config_key()?,
8622                    // v2.67.0 — the `resource` this store RUNS ON. When
8623                    // present the store derives its DSN, its POOL SIZE and its
8624                    // sharing discipline from the resource; `connection:`
8625                    // becomes redundant and `axon-T946` refuses declaring both
8626                    // (the same fact, twice, is how the islands happened).
8627                    "resource" => {
8628                        node.resource_ref = self.consume_any_ident_or_kw()?.value.clone()
8629                    }
8630                    "confidence_floor" => node.confidence_floor = self.parse_optional_float(),
8631                    "isolation" => node.isolation = self.consume_any_ident_or_kw()?.value.clone(),
8632                    "on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
8633                    // v1.30.0 (D11) — Pillar IV: the capability slug
8634                    // required to access this store. Validated against
8635                    // the closed slug grammar shared with `requires:`.
8636                    "capability" => {
8637                        let slug_tok = self.consume(TokenType::StringLit)?.clone();
8638                        if !is_valid_capability_slug(&slug_tok.value) {
8639                            return Err(ParseError {
8640                                message: format!(
8641                                    "Invalid capability slug '{}' in axonstore '{}' \
8642                                     `capability:`. Capability slugs must match \
8643                                     ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
8644                                     lowercase identifiers starting with a letter. Examples: \
8645                                     `admin`, `tenant.read`, `hipaa.phi.read`.",
8646                                    slug_tok.value, node.name
8647                                ),
8648                                line: slug_tok.line,
8649                                column: slug_tok.column,
8650                                ..Default::default()
8651                            });
8652                        }
8653                        node.capability = slug_tok.value.clone();
8654                    }
8655                    _ => self.skip_value(),
8656                }
8657            } else if self.check(TokenType::LBrace) {
8658                self.skip_braced_block()?;
8659            }
8660        }
8661        self.consume(TokenType::RBrace)?;
8662        Ok(node)
8663    }
8664
8665    /// v1.31.0 (D1) — parse the three closed forms of an `axonstore`
8666    /// `schema:` declaration:
8667    ///
8668    ///   * form (a) **inline** — `schema { col: Type [constraint…], … }`
8669    ///   * form (b) **manifest reference** — `schema: "qualified.name"`
8670    ///     (string literal that does NOT start with `env:`)
8671    ///   * form (c) **env-var schema namespace** — `schema: env:VAR`
8672    ///     (unquoted) OR `schema: "env:VAR"` (quoted; the literal
8673    ///     starts with `env:`)
8674    ///
8675    /// Called immediately AFTER `schema` is consumed.
8676    fn parse_store_schema_declaration(
8677        &mut self,
8678        store_name: &str,
8679        sch_line: u32,
8680        sch_col: u32,
8681    ) -> Result<crate::store_schema::StoreColumnSchema, ParseError> {
8682        use crate::store_schema::{StoreColumn, StoreColumnSchema, StoreColumnType};
8683
8684        // — Form (a) — inline column block: `schema { ... }`. —
8685        if self.check(TokenType::LBrace) {
8686            self.consume(TokenType::LBrace)?;
8687            let mut columns: Vec<StoreColumn> = Vec::new();
8688            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8689                let col_tok = self.current().clone();
8690                let col_name = self.consume_any_ident_or_kw()?.value.clone();
8691                self.consume(TokenType::Colon)?;
8692                let type_tok = self.consume_any_ident_or_kw()?.clone();
8693                let col_type = StoreColumnType::from_token(&type_tok.value).ok_or_else(|| {
8694                    let names = StoreColumnType::all_canonical_names();
8695                    let suggestion =
8696                        crate::smart_suggest::suggest_for(&type_tok.value, &names);
8697                    let suggest_suffix = if suggestion.is_empty() {
8698                        String::new()
8699                    } else {
8700                        format!(" {suggestion}")
8701                    };
8702                    let known = names.join(", ");
8703                    ParseError {
8704                        message: format!(
8705                            "Unknown column type `{}` for column `{}` in \
8706                             axonstore `{}` `schema:` block. The closed \
8707                             v1.38.0 column-type catalog \
8708                             is {{{known}}} (plus common lowercase \
8709                             aliases — `int`/`integer`/`int4` for \
8710                             `Int`, `bool`/`boolean` for `Bool`, etc.).\
8711                             {suggest_suffix}",
8712                            type_tok.value, col_name, store_name
8713                        ),
8714                        line: type_tok.line,
8715                        column: type_tok.column,
8716                        ..Default::default()
8717                    }
8718                })?;
8719
8720                // v2.26.0 (D1) — the OPTIONAL `Json<T>` shape LENS on a
8721                // column. `payload: Json<UserEvent>` records the expected
8722                // struct shape; the lens is a compile-time expectation only
8723                // (the column stays physically `jsonb`, navigated totally at
8724                // runtime — doctrine `open_data_is_total`). The shape's
8725                // well-formedness (T is a declared `type`) is `axon-T840`
8726                // in the type-checker — it needs the symbol table. Here we
8727                // only enforce the STRUCTURAL rule: a `<T>` lens may refine
8728                // ONLY a `Json` / `Jsonb` column — `axon-T841` otherwise.
8729                let mut json_shape: Option<String> = None;
8730                if self.check(TokenType::Lt) {
8731                    self.advance();
8732                    let shape_tok = self.consume_any_ident_or_kw()?.clone();
8733                    self.consume(TokenType::Gt)?;
8734                    if matches!(col_type, StoreColumnType::Json | StoreColumnType::Jsonb) {
8735                        json_shape = Some(shape_tok.value.clone());
8736                    } else {
8737                        return Err(ParseError {
8738                            message: format!(
8739                                "axon-T841 a shape lens `<{shape}>` may refine \
8740                                 only a `Json` / `Jsonb` column, but column \
8741                                 `{col}` in axonstore `{store}` is `{ty}`. Drop \
8742                                 the `<{shape}>` (a rigid column already has a \
8743                                 fixed shape), or change the column type to \
8744                                 `Json<{shape}>` if it carries open documents.",
8745                                shape = shape_tok.value,
8746                                col = col_name,
8747                                store = store_name,
8748                                ty = col_type.canonical_name(),
8749                            ),
8750                            line: shape_tok.line,
8751                            column: shape_tok.column,
8752                            ..Default::default()
8753                        });
8754                    }
8755                }
8756
8757                let mut col = StoreColumn {
8758                    name: col_name,
8759                    col_type,
8760                    json_shape,
8761                    primary_key: false,
8762                    auto_increment: false,
8763                    not_null: false,
8764                    unique: false,
8765                    indexed: false,
8766                    default_value: String::new(),
8767                    // v1.31.0 (D1) — `identity` is now a recognized
8768                    // inline keyword (see the constraint loop below).
8769                    // Defaults to false; set to true when the adopter
8770                    // writes `id: BigInt primary_key identity`.
8771                    identity: false,
8772                    line: col_tok.line,
8773                    column: col_tok.column,
8774                };
8775
8776                // Trailing constraints (position-independent), matching
8777                // the Python `_parse_store_column` surface.
8778                while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8779                    if self.current().ttype != TokenType::Identifier {
8780                        // The next column starts with a non-identifier
8781                        // (rare) — stop the constraint scan.
8782                        break;
8783                    }
8784                    let constraint = self.current().value.clone();
8785                    match constraint.as_str() {
8786                        "primary_key" => {
8787                            col.primary_key = true;
8788                            self.advance();
8789                        }
8790                        "auto_increment" => {
8791                            col.auto_increment = true;
8792                            self.advance();
8793                        }
8794                        "not_null" => {
8795                            col.not_null = true;
8796                            self.advance();
8797                        }
8798                        "unique" => {
8799                            col.unique = true;
8800                            self.advance();
8801                        }
8802                        // v2.26.0 (D1) — the `index` constraint declares
8803                        // an index as a capability-honest effect (visible to
8804                        // the deploy gate, not a silent DBA action). The
8805                        // backend picks the method from the column type
8806                        // (GIN for a Json/Jsonb column, b-tree otherwise).
8807                        "index" => {
8808                            col.indexed = true;
8809                            self.advance();
8810                        }
8811                        // v1.31.0 (D1) — `identity` marks a column
8812                        // as `GENERATED ALWAYS/BY DEFAULT AS IDENTITY`.
8813                        // Distinct from `auto_increment` (legacy SERIAL
8814                        // via `nextval(...)` default). T803 skips
8815                        // identity columns from the NOT-NULL-omission
8816                        // check because Postgres auto-fills them; the
8817                        // distinction matters because IDENTITY ALWAYS
8818                        // also rejects user-supplied values, where
8819                        // SERIAL accepts them (a future 38.x.e arm in
8820                        // T802 may surface this).
8821                        "identity" => {
8822                            col.identity = true;
8823                            self.advance();
8824                        }
8825                        "default" => {
8826                            self.advance();
8827                            let dv = self.current().clone();
8828                            if matches!(
8829                                dv.ttype,
8830                                TokenType::StringLit
8831                                    | TokenType::Integer
8832                                    | TokenType::Float
8833                            ) {
8834                                col.default_value = dv.value.clone();
8835                                self.advance();
8836                            } else {
8837                                col.default_value =
8838                                    self.consume_any_ident_or_kw()?.value.clone();
8839                            }
8840                        }
8841                        _ => break,
8842                    }
8843                }
8844
8845                columns.push(col);
8846            }
8847            self.consume(TokenType::RBrace)?;
8848            return Ok(StoreColumnSchema::Inline {
8849                columns,
8850                leading_trivia: Vec::new(),
8851                line: sch_line,
8852                column: sch_col,
8853            });
8854        }
8855
8856        // — Forms (b) + (c) require a `:` separator. —
8857        if !self.check(TokenType::Colon) {
8858            let cur = self.current().clone();
8859            return Err(ParseError {
8860                message: format!(
8861                    "axonstore `{store_name}` `schema:` declaration expects \
8862                     `{{ … }}` (inline columns), `: \"manifest.ref\"` \
8863                     (manifest reference), or `: env:VAR` (per-tenant schema \
8864                     namespace). Got `{}` instead.",
8865                    cur.value
8866                ),
8867                line: cur.line,
8868                column: cur.column,
8869                ..Default::default()
8870            });
8871        }
8872        self.consume(TokenType::Colon)?;
8873
8874        // — Form (b) or (c)-quoted — string literal value. —
8875        if self.check(TokenType::StringLit) {
8876            let lit = self.consume(TokenType::StringLit)?.clone();
8877            let value = lit.value.clone();
8878            if let Some(var) = value.strip_prefix("env:") {
8879                let var = var.trim();
8880                if var.is_empty() {
8881                    return Err(ParseError {
8882                        message: format!(
8883                            "axonstore `{store_name}` `schema: \"env:\"` is \
8884                             missing the variable name after the `env:` \
8885                             prefix."
8886                        ),
8887                        line: lit.line,
8888                        column: lit.column,
8889                        ..Default::default()
8890                    });
8891                }
8892                return Ok(StoreColumnSchema::EnvVar {
8893                    var_name: var.to_string(),
8894                    line: sch_line,
8895                    column: sch_col,
8896                });
8897            }
8898            // Plain string → manifest reference.
8899            if value.trim().is_empty() {
8900                return Err(ParseError {
8901                    message: format!(
8902                        "axonstore `{store_name}` `schema:` manifest reference \
8903                         is empty. Expected `\"qualified.name\"` — e.g. \
8904                         `\"public.tenants\"`."
8905                    ),
8906                    line: lit.line,
8907                    column: lit.column,
8908                    ..Default::default()
8909                });
8910            }
8911            return Ok(StoreColumnSchema::ManifestRef {
8912                qualified_name: value,
8913                line: sch_line,
8914                column: sch_col,
8915            });
8916        }
8917
8918        // — Form (c) unquoted — `env:VAR`. The lexer emits `env` as an
8919        //   identifier, then `:`, then the identifier var name. —
8920        let env_tok = self.current().clone();
8921        if env_tok.value == "env" {
8922            self.advance();
8923            if !self.check(TokenType::Colon) {
8924                return Err(ParseError {
8925                    message: format!(
8926                        "axonstore `{store_name}` `schema: env` is missing the \
8927                         `:` separator. Expected `schema: env:VAR`."
8928                    ),
8929                    line: env_tok.line,
8930                    column: env_tok.column,
8931                    ..Default::default()
8932                });
8933            }
8934            self.advance(); // past ':'
8935            let var_tok = self.consume_any_ident_or_kw()?.clone();
8936            if var_tok.value.trim().is_empty() {
8937                return Err(ParseError {
8938                    message: format!(
8939                        "axonstore `{store_name}` `schema: env:` is missing \
8940                         the variable name."
8941                    ),
8942                    line: var_tok.line,
8943                    column: var_tok.column,
8944                    ..Default::default()
8945                });
8946            }
8947            return Ok(StoreColumnSchema::EnvVar {
8948                var_name: var_tok.value.clone(),
8949                line: sch_line,
8950                column: sch_col,
8951            });
8952        }
8953
8954        Err(ParseError {
8955            message: format!(
8956                "axonstore `{store_name}` `schema:` declaration expects \
8957                 `{{ … }}` (inline columns), `\"manifest.ref\"` (manifest \
8958                 reference), or `env:VAR` (per-tenant schema namespace). \
8959                 Got `{}` instead.",
8960                env_tok.value
8961            ),
8962            line: env_tok.line,
8963            column: env_tok.column,
8964            ..Default::default()
8965        })
8966    }
8967
8968    // ── v1.1.0 — Resource primitive ────────────────────────
8969
8970    /// Parse: `resource Name { kind, endpoint, capacity, lifetime, certainty_floor, shield }`.
8971    ///
8972    /// Mirrors `axon.compiler.parser.Parser._parse_resource`. Unknown fields
8973    /// are silently skipped (keeps the grammar forward-compatible).
8974    fn parse_resource(&mut self) -> Result<ResourceDefinition, ParseError> {
8975        let tok = self.consume(TokenType::Resource)?;
8976        let name = self.consume(TokenType::Identifier)?.value;
8977        let mut node = ResourceDefinition {
8978            name,
8979            kind: String::new(),
8980            endpoint: String::new(),
8981            capacity: None,
8982            lifetime: "affine".to_string(),
8983            certainty_floor: None,
8984            shield_ref: String::new(),
8985            within: String::new(),
8986            loc: Loc {
8987                line: tok.line,
8988                column: tok.column,
8989            },
8990            leading_trivia: Vec::new(),
8991            trailing_trivia: Vec::new(),
8992        };
8993        self.consume(TokenType::LBrace)?;
8994        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8995            let field_tok = self.current().clone();
8996            let field_name = field_tok.value.clone();
8997            self.advance();
8998            if !self.check(TokenType::Colon) {
8999                // Tolerate stray brace or unknown layout.
9000                if self.check(TokenType::LBrace) {
9001                    self.skip_braced_block()?;
9002                }
9003                continue;
9004            }
9005            self.advance(); // past ':'
9006            match field_name.as_str() {
9007                "kind" => node.kind = self.consume_any_ident_or_kw()?.value,
9008                // v2.67.0 — `endpoint:` accepts BOTH shapes on purpose:
9009                //   - a dotted config key  (`endpoint: db.main`)      — the law
9010                //   - a string literal     (`endpoint: "postgres://…"`) — the sin
9011                //
9012                // The literal is REFUSED, but by `axon-T944`, not by the parser.
9013                // If it died here the adopter would read "Expected StringLit",
9014                // which explains nothing. The law gets to say why: *URLs and
9015                // credentials never appear in source* — the same sentence
9016                // `axon-T850` has been saying to `upstream.resolve` all along.
9017                //
9018                // A diagnostic that names the rule teaches; one that names the
9019                // token type only tells you the compiler is unhappy.
9020                "endpoint" => {
9021                    node.endpoint = if self.check(TokenType::StringLit) {
9022                        self.consume(TokenType::StringLit)?.value
9023                    } else {
9024                        self.parse_dotted_identifier()?
9025                    };
9026                }
9027                "capacity" => {
9028                    node.capacity = self.parse_optional_int();
9029                }
9030                "lifetime" => {
9031                    let lt_tok = self.consume_any_ident_or_kw()?;
9032                    let lt = lt_tok.value;
9033                    if !matches!(lt.as_str(), "linear" | "affine" | "persistent") {
9034                        return Err(ParseError {
9035                            message: format!(
9036                                "Invalid lifetime '{lt}' in resource '{}' — \
9037                                 expected linear | affine | persistent",
9038                                node.name
9039                            ),
9040                            line: lt_tok.line,
9041                            column: lt_tok.column,
9042                                                    ..Default::default()
9043                        });
9044                    }
9045                    node.lifetime = lt;
9046                }
9047                "certainty_floor" => {
9048                    node.certainty_floor = self.parse_optional_float();
9049                }
9050                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
9051                // v2.67.0 — `within: <fabric>`. ONE field, so a resource
9052                // cannot be in two fabrics: Separation-Logic disjointness is
9053                // unrepresentable rather than verified.
9054                "within" => node.within = self.consume_any_ident_or_kw()?.value,
9055                // v2.67.0 — an unknown field is a HARD ERROR, not a shrug.
9056                //
9057                // This arm used to be `_ => self.skip_value()`. That is the same
9058                // family as v2.67.0's root cause (`parse_block_step` →
9059                // `skip_braced_block()`, which silently killed four primitives):
9060                // a misspelled `withn:` would have been swallowed without a
9061                // word, and the resource would have governed nothing while
9062                // looking governed. A field the parser does not know is a field
9063                // the adopter believes in and the compiler does not.
9064                unknown => {
9065                    return Err(ParseError {
9066                        message: format!(
9067                            "Unknown field '{unknown}' in resource '{}' — expected one of: \
9068                             kind, endpoint, capacity, lifetime, certainty_floor, shield, within",
9069                            node.name
9070                        ),
9071                        line: field_tok.line,
9072                        column: field_tok.column,
9073                        ..Default::default()
9074                    });
9075                }
9076            }
9077        }
9078        self.consume(TokenType::RBrace)?;
9079        Ok(node)
9080    }
9081
9082    /// Parse: `fabric Name { provider, region, zones, ephemeral, shield }`.
9083    fn parse_fabric(&mut self) -> Result<FabricDefinition, ParseError> {
9084        let tok = self.consume(TokenType::Fabric)?;
9085        let name = self.consume(TokenType::Identifier)?.value;
9086        let mut node = FabricDefinition {
9087            name,
9088            provider: String::new(),
9089            region: String::new(),
9090            zones: None,
9091            ephemeral: None,
9092            shield_ref: String::new(),
9093            loc: Loc {
9094                line: tok.line,
9095                column: tok.column,
9096            },
9097            leading_trivia: Vec::new(),
9098            trailing_trivia: Vec::new(),
9099        };
9100        self.consume(TokenType::LBrace)?;
9101        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9102            let field_name = self.current().value.clone();
9103            self.advance();
9104            if !self.check(TokenType::Colon) {
9105                if self.check(TokenType::LBrace) {
9106                    self.skip_braced_block()?;
9107                }
9108                continue;
9109            }
9110            self.advance(); // past ':'
9111            match field_name.as_str() {
9112                "provider" => node.provider = self.consume_any_ident_or_kw()?.value,
9113                "region" => node.region = self.consume(TokenType::StringLit)?.value,
9114                "zones" => node.zones = self.parse_optional_int(),
9115                "ephemeral" => {
9116                    let b = self.parse_bool()?;
9117                    node.ephemeral = Some(b);
9118                }
9119                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
9120                _ => self.skip_value(),
9121            }
9122        }
9123        self.consume(TokenType::RBrace)?;
9124        Ok(node)
9125    }
9126
9127    /// Parse: `manifest Name { resources, fabric, region, zones, compliance }`.
9128    fn parse_manifest(&mut self) -> Result<ManifestDefinition, ParseError> {
9129        let tok = self.consume(TokenType::Manifest)?;
9130        let name = self.consume(TokenType::Identifier)?.value;
9131        let mut node = ManifestDefinition {
9132            name,
9133            resources: Vec::new(),
9134            fabric_ref: String::new(),
9135            region: String::new(),
9136            zones: None,
9137            compliance: Vec::new(),
9138            loc: Loc {
9139                line: tok.line,
9140                column: tok.column,
9141            },
9142            leading_trivia: Vec::new(),
9143            trailing_trivia: Vec::new(),
9144        };
9145        self.consume(TokenType::LBrace)?;
9146        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9147            let field_name = self.current().value.clone();
9148            self.advance();
9149            if !self.check(TokenType::Colon) {
9150                if self.check(TokenType::LBrace) {
9151                    self.skip_braced_block()?;
9152                }
9153                continue;
9154            }
9155            self.advance();
9156            match field_name.as_str() {
9157                "resources" => node.resources = self.parse_bracketed_identifiers()?,
9158                "fabric" => node.fabric_ref = self.consume_any_ident_or_kw()?.value,
9159                "region" => node.region = self.consume(TokenType::StringLit)?.value,
9160                "zones" => node.zones = self.parse_optional_int(),
9161                "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
9162                _ => self.skip_value(),
9163            }
9164        }
9165        self.consume(TokenType::RBrace)?;
9166        Ok(node)
9167    }
9168
9169    /// Parse: `observe Name from Manifest { sources, quorum, timeout, on_partition, certainty_floor }`.
9170    fn parse_observe(&mut self) -> Result<ObserveDefinition, ParseError> {
9171        let tok = self.consume(TokenType::Observe)?;
9172        let name = self.consume(TokenType::Identifier)?.value;
9173        // `from <Manifest>` — required per Python grammar.
9174        self.consume(TokenType::From)?;
9175        let target = self.consume(TokenType::Identifier)?.value;
9176        let mut node = ObserveDefinition {
9177            name,
9178            target,
9179            sources: Vec::new(),
9180            quorum: None,
9181            timeout: String::new(),
9182            on_partition: "fail".to_string(),
9183            certainty_floor: None,
9184            loc: Loc {
9185                line: tok.line,
9186                column: tok.column,
9187            },
9188            leading_trivia: Vec::new(),
9189            trailing_trivia: Vec::new(),
9190        };
9191        self.consume(TokenType::LBrace)?;
9192        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9193            let field_name = self.current().value.clone();
9194            self.advance();
9195            if !self.check(TokenType::Colon) {
9196                if self.check(TokenType::LBrace) {
9197                    self.skip_braced_block()?;
9198                }
9199                continue;
9200            }
9201            self.advance();
9202            match field_name.as_str() {
9203                "sources" => node.sources = self.parse_bracketed_identifiers()?,
9204                "quorum" => node.quorum = self.parse_optional_int(),
9205                "timeout" => {
9206                    let t = self.current().clone();
9207                    match t.ttype {
9208                        TokenType::Duration | TokenType::StringLit => {
9209                            self.advance();
9210                            node.timeout = t.value;
9211                        }
9212                        _ => node.timeout = self.consume_any_ident_or_kw()?.value,
9213                    }
9214                }
9215                "on_partition" => {
9216                    let p_tok = self.consume_any_ident_or_kw()?;
9217                    let p = p_tok.value;
9218                    if !matches!(p.as_str(), "fail" | "shield_quarantine") {
9219                        return Err(ParseError {
9220                            message: format!(
9221                                "Invalid on_partition '{p}' in observe '{}' — \
9222                                 expected fail | shield_quarantine",
9223                                node.name
9224                            ),
9225                            line: p_tok.line,
9226                            column: p_tok.column,
9227                                                    ..Default::default()
9228                        });
9229                    }
9230                    node.on_partition = p;
9231                }
9232                "certainty_floor" => node.certainty_floor = self.parse_optional_float(),
9233                _ => self.skip_value(),
9234            }
9235        }
9236        self.consume(TokenType::RBrace)?;
9237        Ok(node)
9238    }
9239
9240    // ── v1.1.0 — Control cognitivo ─────────────────────────
9241
9242    /// Parse: `reconcile Name { observe, threshold, tolerance, on_drift, shield, mandate, max_retries }`.
9243    fn parse_reconcile(&mut self) -> Result<ReconcileDefinition, ParseError> {
9244        let tok = self.consume(TokenType::Reconcile)?;
9245        let name = self.consume(TokenType::Identifier)?.value;
9246        let mut node = ReconcileDefinition {
9247            name,
9248            observe_ref: String::new(),
9249            threshold: None,
9250            tolerance: None,
9251            on_drift: "provision".to_string(),
9252            shield_ref: String::new(),
9253            mandate_ref: String::new(),
9254            max_retries: 3,
9255            loc: Loc {
9256                line: tok.line,
9257                column: tok.column,
9258            },
9259            leading_trivia: Vec::new(),
9260            trailing_trivia: Vec::new(),
9261        };
9262        self.consume(TokenType::LBrace)?;
9263        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9264            let field_name = self.current().value.clone();
9265            self.advance();
9266            if !self.check(TokenType::Colon) {
9267                if self.check(TokenType::LBrace) {
9268                    self.skip_braced_block()?;
9269                }
9270                continue;
9271            }
9272            self.advance();
9273            match field_name.as_str() {
9274                "observe" => node.observe_ref = self.consume_any_ident_or_kw()?.value,
9275                "threshold" => node.threshold = self.parse_optional_float(),
9276                "tolerance" => node.tolerance = self.parse_optional_float(),
9277                "on_drift" => {
9278                    let d_tok = self.consume_any_ident_or_kw()?;
9279                    let d = d_tok.value;
9280                    if !matches!(d.as_str(), "provision" | "alert" | "refine") {
9281                        return Err(ParseError {
9282                            message: format!(
9283                                "Invalid on_drift '{d}' in reconcile '{}' — \
9284                                 expected provision | alert | refine",
9285                                node.name
9286                            ),
9287                            line: d_tok.line,
9288                            column: d_tok.column,
9289                                                    ..Default::default()
9290                        });
9291                    }
9292                    node.on_drift = d;
9293                }
9294                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
9295                "mandate" => node.mandate_ref = self.consume_any_ident_or_kw()?.value,
9296                "max_retries" => {
9297                    if let Some(v) = self.parse_optional_int() {
9298                        node.max_retries = v;
9299                    }
9300                }
9301                _ => self.skip_value(),
9302            }
9303        }
9304        self.consume(TokenType::RBrace)?;
9305        Ok(node)
9306    }
9307
9308    /// Parse: `lease Name { resource, duration, acquire, on_expire }`.
9309    fn parse_lease(&mut self) -> Result<LeaseDefinition, ParseError> {
9310        let tok = self.consume(TokenType::Lease)?;
9311        let name = self.consume(TokenType::Identifier)?.value;
9312        let mut node = LeaseDefinition {
9313            name,
9314            resource_ref: String::new(),
9315            duration: String::new(),
9316            acquire: "on_start".to_string(),
9317            on_expire: "anchor_breach".to_string(),
9318            loc: Loc {
9319                line: tok.line,
9320                column: tok.column,
9321            },
9322            leading_trivia: Vec::new(),
9323            trailing_trivia: Vec::new(),
9324        };
9325        self.consume(TokenType::LBrace)?;
9326        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9327            let field_name = self.current().value.clone();
9328            self.advance();
9329            if !self.check(TokenType::Colon) {
9330                if self.check(TokenType::LBrace) {
9331                    self.skip_braced_block()?;
9332                }
9333                continue;
9334            }
9335            self.advance();
9336            match field_name.as_str() {
9337                "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
9338                "duration" => {
9339                    let t = self.current().clone();
9340                    match t.ttype {
9341                        TokenType::Duration | TokenType::StringLit => {
9342                            self.advance();
9343                            node.duration = t.value;
9344                        }
9345                        _ => node.duration = self.consume_any_ident_or_kw()?.value,
9346                    }
9347                }
9348                "acquire" => {
9349                    let a_tok = self.consume_any_ident_or_kw()?;
9350                    let a = a_tok.value;
9351                    if !matches!(a.as_str(), "on_start" | "on_demand") {
9352                        return Err(ParseError {
9353                            message: format!(
9354                                "Invalid acquire '{a}' in lease '{}' — \
9355                                 expected on_start | on_demand",
9356                                node.name
9357                            ),
9358                            line: a_tok.line,
9359                            column: a_tok.column,
9360                                                    ..Default::default()
9361                        });
9362                    }
9363                    node.acquire = a;
9364                }
9365                "on_expire" => {
9366                    let e_tok = self.consume_any_ident_or_kw()?;
9367                    let e = e_tok.value;
9368                    if !matches!(e.as_str(), "anchor_breach" | "release" | "extend") {
9369                        return Err(ParseError {
9370                            message: format!(
9371                                "Invalid on_expire '{e}' in lease '{}' — \
9372                                 expected anchor_breach | release | extend",
9373                                node.name
9374                            ),
9375                            line: e_tok.line,
9376                            column: e_tok.column,
9377                                                    ..Default::default()
9378                        });
9379                    }
9380                    node.on_expire = e;
9381                }
9382                _ => self.skip_value(),
9383            }
9384        }
9385        self.consume(TokenType::RBrace)?;
9386        Ok(node)
9387    }
9388
9389    /// Parse: `ensemble Name { observations, quorum, aggregation, certainty_mode }`.
9390    fn parse_ensemble(&mut self) -> Result<EnsembleDefinition, ParseError> {
9391        let tok = self.consume(TokenType::Ensemble)?;
9392        let name = self.consume(TokenType::Identifier)?.value;
9393        let mut node = EnsembleDefinition {
9394            name,
9395            observations: Vec::new(),
9396            quorum: None,
9397            aggregation: "majority".to_string(),
9398            certainty_mode: "min".to_string(),
9399            loc: Loc {
9400                line: tok.line,
9401                column: tok.column,
9402            },
9403            leading_trivia: Vec::new(),
9404            trailing_trivia: Vec::new(),
9405        };
9406        self.consume(TokenType::LBrace)?;
9407        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9408            let field_name = self.current().value.clone();
9409            self.advance();
9410            if !self.check(TokenType::Colon) {
9411                if self.check(TokenType::LBrace) {
9412                    self.skip_braced_block()?;
9413                }
9414                continue;
9415            }
9416            self.advance();
9417            match field_name.as_str() {
9418                "observations" => node.observations = self.parse_bracketed_identifiers()?,
9419                "quorum" => node.quorum = self.parse_optional_int(),
9420                "aggregation" => {
9421                    let a_tok = self.consume_any_ident_or_kw()?;
9422                    let a = a_tok.value;
9423                    if !matches!(a.as_str(), "majority" | "weighted" | "byzantine") {
9424                        return Err(ParseError {
9425                            message: format!(
9426                                "Invalid aggregation '{a}' in ensemble '{}' — \
9427                                 expected majority | weighted | byzantine",
9428                                node.name
9429                            ),
9430                            line: a_tok.line,
9431                            column: a_tok.column,
9432                                                    ..Default::default()
9433                        });
9434                    }
9435                    node.aggregation = a;
9436                }
9437                "certainty_mode" => {
9438                    let c_tok = self.consume_any_ident_or_kw()?;
9439                    let c = c_tok.value;
9440                    if !matches!(c.as_str(), "min" | "weighted" | "harmonic") {
9441                        return Err(ParseError {
9442                            message: format!(
9443                                "Invalid certainty_mode '{c}' in ensemble '{}' — \
9444                                 expected min | weighted | harmonic",
9445                                node.name
9446                            ),
9447                            line: c_tok.line,
9448                            column: c_tok.column,
9449                                                    ..Default::default()
9450                        });
9451                    }
9452                    node.certainty_mode = c;
9453                }
9454                _ => self.skip_value(),
9455            }
9456        }
9457        self.consume(TokenType::RBrace)?;
9458        Ok(node)
9459    }
9460
9461    // ── v1.1.0 — Topology + π-calculus binary sessions ─────
9462
9463    /// Parse: `session Name { role1: [step, …]  role2: [step, …] }`.
9464    ///
9465    /// The enclosing `parse_session_definition` disambiguates from the session
9466    /// step token `session` (which does not exist) by always entering from the
9467    /// top-level dispatch; the identifier role name is consumed after `{`.
9468    fn parse_session_definition(&mut self) -> Result<SessionDefinition, ParseError> {
9469        let tok = self.consume(TokenType::Session)?;
9470        let name = self.consume(TokenType::Identifier)?.value;
9471        let mut node = SessionDefinition {
9472            name,
9473            roles: Vec::new(),
9474            loc: Loc {
9475                line: tok.line,
9476                column: tok.column,
9477            },
9478            leading_trivia: Vec::new(),
9479            trailing_trivia: Vec::new(),
9480        };
9481        self.consume(TokenType::LBrace)?;
9482        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9483            let role_tok = self.consume_any_ident_or_kw()?;
9484            self.consume(TokenType::Colon)?;
9485            let steps = self.parse_session_steps()?;
9486            node.roles.push(SessionRole {
9487                name: role_tok.value,
9488                steps,
9489                loc: Loc {
9490                    line: role_tok.line,
9491                    column: role_tok.column,
9492                },
9493            });
9494        }
9495        self.consume(TokenType::RBrace)?;
9496        Ok(node)
9497    }
9498
9499    /// v2.4.0 — Parse a Pauli-sum observable declaration:
9500    /// ```text
9501    /// observable EnergyHamiltonian {
9502    ///     qubits: 2
9503    ///     term: 0.5 * "ZZ"
9504    ///     term: -1.2 * "XI"
9505    /// }
9506    /// ```
9507    /// `term:` is a repeatable key (one `cₖ · Pₖ` per line). The coefficient is
9508    /// a real scalar (optional leading `+`/`-`), then `*`, then a quoted Pauli
9509    /// string. The type-checker (v2.4.0) validates the closed `{I,X,Y,Z}`
9510    /// alphabet + equal lengths; real coefficients ⇒ Hermitian by construction.
9511    fn parse_observable(&mut self) -> Result<ObservableDefinition, ParseError> {
9512        let tok = self.consume(TokenType::Observable)?;
9513        let name = self.consume(TokenType::Identifier)?.value;
9514        let mut node = ObservableDefinition {
9515            name,
9516            qubits: None,
9517            terms: Vec::new(),
9518            loc: Loc {
9519                line: tok.line,
9520                column: tok.column,
9521            },
9522            leading_trivia: Vec::new(),
9523            trailing_trivia: Vec::new(),
9524        };
9525        self.consume(TokenType::LBrace)?;
9526        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9527            let key_tok = self.consume_any_ident_or_kw()?;
9528            self.consume(TokenType::Colon)?;
9529            match key_tok.value.as_str() {
9530                "qubits" => node.qubits = Some(self.consume_number()? as i64),
9531                "term" => {
9532                    let term_loc = Loc {
9533                        line: key_tok.line,
9534                        column: key_tok.column,
9535                    };
9536                    // Optional sign, then magnitude.
9537                    let mut negative = false;
9538                    if self.check(TokenType::Minus) {
9539                        self.advance();
9540                        negative = true;
9541                    } else if self.check(TokenType::Plus) {
9542                        self.advance();
9543                    }
9544                    let mag = self.consume_number()?;
9545                    let coefficient = if negative { -mag } else { mag };
9546                    // `*` separator between coefficient and Pauli string.
9547                    self.consume(TokenType::Star)?;
9548                    let pauli = self.consume(TokenType::StringLit)?.value;
9549                    node.terms.push(PauliTerm {
9550                        coefficient,
9551                        pauli,
9552                        loc: term_loc,
9553                    });
9554                }
9555                _ => self.skip_value(),
9556            }
9557        }
9558        self.consume(TokenType::RBrace)?;
9559        Ok(node)
9560    }
9561
9562    /// v2.23.0 — Parse:
9563    /// `witness Name { claim: <ref>  against: <baseline>  metric: <metric>
9564    ///                 threshold: <ε>  data: <source> }`.
9565    /// Order-free `key: value` pairs. `claim`/`against`/`metric`/`data` are bare
9566    /// identifiers (a ref or a closed-catalog keyword); `threshold` is a number.
9567    /// Well-formedness (known metric, threshold range, required fields) is the
9568    /// type-checker's job (`axon-E0790`).
9569    fn parse_witness(&mut self) -> Result<WitnessDefinition, ParseError> {
9570        let tok = self.consume(TokenType::Witness)?;
9571        let name = self.consume(TokenType::Identifier)?.value;
9572        let mut node = WitnessDefinition {
9573            name,
9574            claim: String::new(),
9575            baseline: String::new(),
9576            metric: String::new(),
9577            threshold: 0.0,
9578            data: String::new(),
9579            loc: Loc {
9580                line: tok.line,
9581                column: tok.column,
9582            },
9583            leading_trivia: Vec::new(),
9584            trailing_trivia: Vec::new(),
9585        };
9586        self.consume(TokenType::LBrace)?;
9587        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9588            let key_tok = self.consume_any_ident_or_kw()?;
9589            self.consume(TokenType::Colon)?;
9590            match key_tok.value.as_str() {
9591                "claim" => node.claim = self.consume_any_ident_or_kw()?.value,
9592                // `against` is the baseline; `against` is not a reserved keyword,
9593                // so it lexes as an identifier key here.
9594                "against" => node.baseline = self.consume_any_ident_or_kw()?.value,
9595                "metric" => node.metric = self.consume_any_ident_or_kw()?.value,
9596                "threshold" => node.threshold = self.consume_number()?,
9597                "data" => node.data = self.consume_any_ident_or_kw()?.value,
9598                _ => self.skip_value(),
9599            }
9600        }
9601        self.consume(TokenType::RBrace)?;
9602        Ok(node)
9603    }
9604
9605    /// v2.3.0 — Parse:
9606    /// `socket Name { protocol: SessionRef, backpressure: credit(n),
9607    ///               reconnect: cognitive_state, legal_basis: ... }`.
9608    /// Fields are `key: value` pairs (order-free); only `protocol` is required.
9609    fn parse_socket(&mut self) -> Result<SocketDefinition, ParseError> {
9610        let tok = self.consume(TokenType::Socket)?;
9611        let name = self.consume(TokenType::Identifier)?.value;
9612        let mut node = SocketDefinition {
9613            name,
9614            loc: Loc { line: tok.line, column: tok.column },
9615            ..Default::default()
9616        };
9617        self.consume(TokenType::LBrace)?;
9618        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9619            let key = self.consume_any_ident_or_kw()?.value;
9620            self.consume(TokenType::Colon)?;
9621            match key.as_str() {
9622                "protocol" => node.protocol = self.consume_any_ident_or_kw()?.value,
9623                "backpressure" => {
9624                    // `credit(n)` — the typed-resource window.
9625                    let kind = self.consume_any_ident_or_kw()?.value;
9626                    if kind != "credit" {
9627                        return Err(self.error(&format!("expected `credit(n)` for backpressure, got `{kind}`")));
9628                    }
9629                    self.consume(TokenType::LParen)?;
9630                    let n = self
9631                        .consume(TokenType::Integer)?
9632                        .value
9633                        .parse::<i64>()
9634                        .map_err(|_| self.error("backpressure credit must be an integer"))?;
9635                    self.consume(TokenType::RParen)?;
9636                    node.backpressure_credit = Some(n);
9637                }
9638                "reconnect" => {
9639                    let mode = self.consume_any_ident_or_kw()?.value;
9640                    node.reconnect = mode == "cognitive_state";
9641                }
9642                "legal_basis" => node.legal_basis = Some(self.consume_any_ident_or_kw()?.value),
9643                other => return Err(self.error(&format!("unknown socket field `{other}`"))),
9644            }
9645            // Optional comma between fields.
9646            if self.check(TokenType::Comma) {
9647                self.consume(TokenType::Comma)?;
9648            }
9649        }
9650        self.consume(TokenType::RBrace)?;
9651        Ok(node)
9652    }
9653
9654    /// v2.37.0 — parse `upstream Name [from Preset@vN] { fields }`.
9655    ///
9656    /// Field grammar per `the design plan` section 1–2. The
9657    /// parser fixes the *shape* only; catalog membership (`transport:`,
9658    /// `auth:`, `overflow:`, `on_exhausted:`), key charsets and projection
9659    /// totality are v2.37.0 type-checker laws (T849–T851), mirroring how
9660    /// `socket` splits parse vs. check.
9661    fn parse_upstream(&mut self) -> Result<UpstreamDefinition, ParseError> {
9662        let tok = self.consume(TokenType::Upstream)?;
9663        let name = self.consume(TokenType::Identifier)?.value;
9664        let mut node = UpstreamDefinition {
9665            name,
9666            loc: Loc { line: tok.line, column: tok.column },
9667            ..Default::default()
9668        };
9669        // v2.37.0 — preset instantiation: `upstream X from DeepgramSTT@v1 {…}`.
9670        if self.check(TokenType::From) {
9671            self.advance();
9672            let base = self.consume(TokenType::Identifier)?.value;
9673            self.consume(TokenType::At)?;
9674            let version = self.consume_any_ident_or_kw()?.value;
9675            node.preset = Some(format!("{base}@{version}"));
9676        }
9677        self.consume(TokenType::LBrace)?;
9678        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9679            let key = self.consume_any_ident_or_kw()?.value;
9680            self.consume(TokenType::Colon)?;
9681            match key.as_str() {
9682                "transport" => node.transport = self.consume_any_ident_or_kw()?.value,
9683                "protocol" => node.protocol = self.consume_any_ident_or_kw()?.value,
9684                "role" => node.role = self.consume_any_ident_or_kw()?.value,
9685                "resolve" => node.resolve = self.parse_dotted_identifier()?,
9686                // v2.69.0 — the upstream's channel rides a declared
9687                // `resource`; the address + instance bound DERIVE from it.
9688                // XOR with `resolve:` is axon-T951 (type-checker territory).
9689                "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
9690                "secret" => node.secret = self.parse_dotted_identifier()?,
9691                "auth" => {
9692                    // `header("Name")` | `header("Name", "Prefix ")` |
9693                    // `query("param")` | `signed_url`.
9694                    node.auth_kind = self.consume_any_ident_or_kw()?.value;
9695                    if self.check(TokenType::LParen) {
9696                        self.consume(TokenType::LParen)?;
9697                        node.auth_name = Some(self.consume(TokenType::StringLit)?.value);
9698                        if self.check(TokenType::Comma) {
9699                            self.consume(TokenType::Comma)?;
9700                            node.auth_prefix = Some(self.consume(TokenType::StringLit)?.value);
9701                        }
9702                        self.consume(TokenType::RParen)?;
9703                    }
9704                }
9705                "map" => node.map = self.parse_upstream_map()?,
9706                "reconnect" => node.reconnect = Some(self.parse_upstream_reconnect()?),
9707                "overflow" => node.overflow = Some(self.consume_any_ident_or_kw()?.value),
9708                "backpressure" => {
9709                    // `credit(n)` — same typed-resource window as `socket`.
9710                    let kind = self.consume_any_ident_or_kw()?.value;
9711                    if kind != "credit" {
9712                        return Err(self.error(&format!("expected `credit(n)` for backpressure, got `{kind}`")));
9713                    }
9714                    self.consume(TokenType::LParen)?;
9715                    let n = self
9716                        .consume(TokenType::Integer)?
9717                        .value
9718                        .parse::<i64>()
9719                        .map_err(|_| self.error("backpressure credit must be an integer"))?;
9720                    self.consume(TokenType::RParen)?;
9721                    node.backpressure_credit = Some(n);
9722                }
9723                other => return Err(self.error(&format!("unknown upstream field `{other}`"))),
9724            }
9725            // Optional comma between fields.
9726            if self.check(TokenType::Comma) {
9727                self.consume(TokenType::Comma)?;
9728            }
9729        }
9730        self.consume(TokenType::RBrace)?;
9731        Ok(node)
9732    }
9733
9734    /// v2.38.0 — parse `cors Name { fields }`. Field-shape checks
9735    /// (wildcard+credentials, origin-glob shape, closed method catalog,
9736    /// cross-method path consistency) are v2.38.0 type-checker territory
9737    /// (T853-T857); the parser only builds the structural AST.
9738    ///
9739    /// **Unknown fields are a hard error** (the design decision, not `shield`'s lenient
9740    /// `axon-W010` record-and-skip) — mirrors `upstream`'s stricter
9741    /// posture, appropriate for a security-relevant declaration.
9742    fn parse_cors(&mut self) -> Result<CorsDefinition, ParseError> {
9743        let tok = self.consume(TokenType::Cors)?;
9744        let name = self.consume(TokenType::Identifier)?.value;
9745        let mut node = CorsDefinition {
9746            name,
9747            loc: Loc { line: tok.line, column: tok.column },
9748            ..Default::default()
9749        };
9750        self.consume(TokenType::LBrace)?;
9751        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9752            let key = self.consume_any_ident_or_kw()?.value;
9753            self.consume(TokenType::Colon)?;
9754            match key.as_str() {
9755                "allow_origins" => node.allow_origins = self.parse_bracketed_strings()?,
9756                "allow_methods" => node.allow_methods = self.parse_bracketed_identifiers()?,
9757                "allow_headers" => node.allow_headers = self.parse_bracketed_strings()?,
9758                "allow_credentials" => {
9759                    node.allow_credentials = self.consume_any_ident_or_kw()?.value == "true"
9760                }
9761                "max_age" => node.max_age = Some(self.consume(TokenType::Duration)?.value),
9762                "expose_headers" => node.expose_headers = self.parse_bracketed_strings()?,
9763                other => return Err(self.error(&format!("unknown cors field `{other}`"))),
9764            }
9765            // Optional comma between fields.
9766            if self.check(TokenType::Comma) {
9767                self.consume(TokenType::Comma)?;
9768            }
9769        }
9770        self.consume(TokenType::RBrace)?;
9771        Ok(node)
9772    }
9773
9774    /// v2.46.0 — parse `credential Name { ttl: grants: }`. Strict
9775    /// closed-catalog (unknown field is a hard error, the v2.38.0 the design decision
9776    /// discipline — a credential contract governs AUTHORITY, so a typo can
9777    /// never silently produce a permissive contract). `grants:` slugs are
9778    /// validated at parse time with the same closed grammar as
9779    /// `axonendpoint requires:`; the cross-field laws (non-empty grants,
9780    /// TTL bounds) are v2.46.0 type-checker territory (`axon-T893`/`T894`).
9781    fn parse_credential(&mut self) -> Result<CredentialDefinition, ParseError> {
9782        let tok = self.consume(TokenType::Credential)?;
9783        let name = self.consume(TokenType::Identifier)?.value;
9784        let mut node = CredentialDefinition {
9785            name,
9786            loc: Loc { line: tok.line, column: tok.column },
9787            ..Default::default()
9788        };
9789        self.consume(TokenType::LBrace)?;
9790        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9791            let key = self.consume_any_ident_or_kw()?.value;
9792            self.consume(TokenType::Colon)?;
9793            match key.as_str() {
9794                "ttl" => node.ttl = self.consume(TokenType::Duration)?.value,
9795                "grants" => {
9796                    let bracket_tok = self.current().clone();
9797                    let items = self.parse_bracketed_dot_identifiers()?;
9798                    for slug in &items {
9799                        if !is_valid_capability_slug(slug) {
9800                            return Err(ParseError {
9801                                message: format!(
9802                                    "Invalid capability slug '{slug}' in credential '{}' \
9803                                     `grants:`. Capability slugs must match \
9804                                     ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
9805                                     lowercase identifiers starting with a letter. Examples: \
9806                                     `chat.invoke`, `flow.execute`.",
9807                                    node.name
9808                                ),
9809                                line: bracket_tok.line,
9810                                column: bracket_tok.column,
9811                                ..Default::default()
9812                            });
9813                        }
9814                    }
9815                    node.grants = items;
9816                }
9817                other => return Err(self.error(&format!("unknown credential field `{other}`"))),
9818            }
9819            // Optional comma between fields.
9820            if self.check(TokenType::Comma) {
9821                self.consume(TokenType::Comma)?;
9822            }
9823        }
9824        self.consume(TokenType::RBrace)?;
9825        Ok(node)
9826    }
9827
9828    /// v2.40.0 — parse `cache Name { backend:, ttl:, key:, default:,
9829    /// apply_to_effects:, invalidate_on: }`. Strict closed-catalog (unknown
9830    /// field is a hard error, the v2.38.0 the design decision discipline — a cache governs
9831    /// correctness, so a typo can never silently mean "no policy"). All
9832    /// cross-field laws (single default, non-pure-needs-ttl, reference
9833    /// resolution, effect widening) are v2.40.0 type-checker territory.
9834    fn parse_cache(&mut self) -> Result<CacheDefinition, ParseError> {
9835        let tok = self.consume(TokenType::Cache)?;
9836        let name = self.consume(TokenType::Identifier)?.value;
9837        let mut node = CacheDefinition {
9838            name,
9839            loc: Loc { line: tok.line, column: tok.column },
9840            ..Default::default()
9841        };
9842        self.consume(TokenType::LBrace)?;
9843        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9844            let key = self.consume_any_ident_or_kw()?.value;
9845            self.consume(TokenType::Colon)?;
9846            match key.as_str() {
9847                "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
9848                "ttl" => node.ttl = Some(self.consume(TokenType::Duration)?.value),
9849                "key" => node.key_params = self.parse_bracketed_identifiers()?,
9850                "default" => {
9851                    node.default_policy = self.consume_any_ident_or_kw()?.value == "true"
9852                }
9853                "apply_to_effects" => {
9854                    node.apply_to_effects = self.parse_bracketed_identifiers()?
9855                }
9856                "invalidate_on" => node.invalidate_on = self.parse_bracketed_identifiers()?,
9857                other => return Err(self.error(&format!("unknown cache field `{other}`"))),
9858            }
9859            if self.check(TokenType::Comma) {
9860                self.consume(TokenType::Comma)?;
9861            }
9862        }
9863        self.consume(TokenType::RBrace)?;
9864        Ok(node)
9865    }
9866
9867    // ── v2.53.0 — Native Document Synthesis ─────────────────────────────
9868
9869    /// v2.53.0 — parse `document <Name> { target:, template:?, provenance:?,
9870    /// effects:?, <body blocks> }`. Document-level scalars are handled here;
9871    /// anything of the form `ident { … }` is a body block ([`parse_doc_block_body`]).
9872    /// Unknown scalar fields are a hard error (the v2.38.0/v2.39.0 closed-catalog
9873    /// discipline); the per-`target` block vocabulary is the v2.53.0 checker's job.
9874    fn parse_document(&mut self) -> Result<crate::ast::DocumentDefinition, ParseError> {
9875        let tok = self.consume(TokenType::Document)?;
9876        let name = self.consume(TokenType::Identifier)?.value;
9877        let mut node = crate::ast::DocumentDefinition {
9878            name,
9879            loc: Loc {
9880                line: tok.line,
9881                column: tok.column,
9882            },
9883            ..Default::default()
9884        };
9885        self.consume(TokenType::LBrace)?;
9886        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9887            let field = self.current().clone();
9888            let field_name = field.value.clone();
9889            self.advance();
9890            if self.check(TokenType::Colon) {
9891                self.advance();
9892                match field_name.as_str() {
9893                    "target" => node.target = self.consume_any_ident_or_kw()?.value,
9894                    "template" => node.template = self.parse_dotted_identifier()?,
9895                    "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
9896                    "effects" => node.effects = Some(self.parse_effect_row()?),
9897                    other => {
9898                        return Err(self.error(&format!(
9899                            "unknown document field `{other}` in document `{}` — expected \
9900                             `target:` / `template:` / `provenance:` / `effects:`, or a body \
9901                             block (`section {{ … }}` / `slide {{ … }}` / `sheet {{ … }}`)",
9902                            node.name
9903                        )))
9904                    }
9905                }
9906            } else if self.check(TokenType::LBrace) {
9907                node.blocks
9908                    .push(self.parse_doc_block_body(field_name, field.line, field.column)?);
9909            } else {
9910                return Err(self.error(&format!(
9911                    "unexpected `{field_name}` in document `{}` body — expected a `field:` or a \
9912                     body block `{field_name} {{ … }}`",
9913                    node.name
9914                )));
9915            }
9916            if self.check(TokenType::Comma) {
9917                self.advance();
9918            }
9919        }
9920        self.consume(TokenType::RBrace)?;
9921        Ok(node)
9922    }
9923
9924    /// v2.53.0 — parse a document body block whose `kind` was already
9925    /// consumed: `{ (field: value | nested-block { … })* }`. Recursive — a
9926    /// `section` holds `para`/`table`/`chart`; a `slide` holds `bullets`/
9927    /// `notes`; a `sheet` holds `row`/`formula`. A member is a field iff a
9928    /// `:` follows its name; else it must open a nested block (`{`).
9929    fn parse_doc_block_body(
9930        &mut self,
9931        kind: String,
9932        line: u32,
9933        column: u32,
9934    ) -> Result<crate::ast::DocBlock, ParseError> {
9935        let mut block = crate::ast::DocBlock {
9936            kind,
9937            loc: Loc { line, column },
9938            ..Default::default()
9939        };
9940        self.consume(TokenType::LBrace)?;
9941        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9942            let name_tok = self.current().clone();
9943            let name = self.consume_any_ident_or_kw()?.value;
9944            if self.check(TokenType::Colon) {
9945                self.advance();
9946                let value = self.parse_doc_scalar()?;
9947                block.fields.push((name, value));
9948            } else if self.check(TokenType::LBrace) {
9949                let child = self.parse_doc_block_body(name, name_tok.line, name_tok.column)?;
9950                block.children.push(child);
9951            } else {
9952                return Err(self.error(&format!(
9953                    "in document block `{}`: `{name}` must be a `field:` value or open a nested \
9954                     block `{name} {{ … }}`",
9955                    block.kind
9956                )));
9957            }
9958            if self.check(TokenType::Comma) {
9959                self.advance();
9960            }
9961        }
9962        self.consume(TokenType::RBrace)?;
9963        Ok(block)
9964    }
9965
9966    /// v2.53.0 — parse a document field value into a [`crate::ast::DocScalar`].
9967    /// A bare identifier is a REFERENCE (`text: revenue_summary`) — this is what
9968    /// the assertion-laundering barrier inspects; a quoted string / int / bool /
9969    /// bracketed list are literals.
9970    fn parse_doc_scalar(&mut self) -> Result<crate::ast::DocScalar, ParseError> {
9971        let tok = self.current().clone();
9972        match tok.ttype {
9973            TokenType::StringLit => {
9974                self.advance();
9975                Ok(crate::ast::DocScalar::Text(tok.value))
9976            }
9977            TokenType::Integer => {
9978                self.advance();
9979                Ok(crate::ast::DocScalar::Int(tok.value.parse::<i64>().unwrap_or(0)))
9980            }
9981            TokenType::Bool => {
9982                self.advance();
9983                Ok(crate::ast::DocScalar::Bool(tok.value == "true"))
9984            }
9985            TokenType::LBracket => {
9986                let items = self.parse_bracketed_strings()?;
9987                Ok(crate::ast::DocScalar::List(items))
9988            }
9989            _ => {
9990                let name = self.consume_any_ident_or_kw()?.value;
9991                Ok(crate::ast::DocScalar::Ref(name))
9992            }
9993        }
9994    }
9995
9996    // ── v2.60.0 — Governed CRM Delivery ──────────────────────────────────
9997
9998    /// v2.60.0 — parse `deliver <Name> { target:, provenance:?, secret:,
9999    /// effects:?, <operation blocks> }`. Delivery-level scalars are handled here;
10000    /// anything of the form `ident { … }` is an operation block
10001    /// ([`parse_deliver_op`]). Unknown scalar fields are a hard error (the v2.53.0
10002    /// v2.66.0 — the governed human-notification declaration:
10003    ///
10004    /// ```text
10005    /// notify LowSales {
10006    ///     channel:    sms | whatsapp | telegram
10007    ///     to:         secret(ops.oncall_phone)
10008    ///     template:   "Ventas 7d: ${resumen}"
10009    ///     window:     4h
10010    ///     provenance: attached | cleared
10011    ///     effects:    <web>
10012    /// }
10013    /// ```
10014    ///
10015    /// The closed-field discipline (v2.53.0/v2.60.0): an unknown scalar field is
10016    /// a hard parse error. The LAWS (T933/T934/T935) live in the checker
10017    /// so violations accumulate; the parser records shape (including a
10018    /// literal `to:` — kept so T934 can refuse it TEACHING the custody
10019    /// form, instead of a bare parse error).
10020    fn parse_notify(&mut self) -> Result<crate::ast::NotifyDefinition, ParseError> {
10021        let tok = self.consume(TokenType::Notify)?;
10022        let name = self.consume(TokenType::Identifier)?.value;
10023        let mut node = crate::ast::NotifyDefinition {
10024            name,
10025            loc: Loc {
10026                line: tok.line,
10027                column: tok.column,
10028            },
10029            ..Default::default()
10030        };
10031        self.consume(TokenType::LBrace)?;
10032        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10033            let field = self.current().clone();
10034            let field_name = field.value.clone();
10035            self.advance();
10036            if self.check(TokenType::Colon) {
10037                self.advance();
10038                match field_name.as_str() {
10039                    "channel" => node.channel = self.consume_any_ident_or_kw()?.value,
10040                    "to" => {
10041                        // The custody form: `secret(<dotted-class>)`. A string
10042                        // literal parses too — the checker refuses it (T934)
10043                        // with the teaching message.
10044                        if self.current().value == "secret" && self.peek_is_lparen() {
10045                            self.advance(); // `secret`
10046                            self.consume(TokenType::LParen)?;
10047                            node.to_secret = self.parse_dotted_identifier()?;
10048                            self.consume(TokenType::RParen)?;
10049                            node.to_is_secret = true;
10050                        } else if self.check(TokenType::StringLit) {
10051                            node.to_secret = self.consume(TokenType::StringLit)?.value.clone();
10052                            node.to_is_secret = false;
10053                        } else {
10054                            node.to_secret = self.consume_any_ident_or_kw()?.value.clone();
10055                            node.to_is_secret = false;
10056                        }
10057                    }
10058                    "template" => {
10059                        node.template = self.consume(TokenType::StringLit)?.value.clone()
10060                    }
10061                    "window" => {
10062                        // `4h` lexes as Integer + ident or one ident — accept
10063                        // both spellings, normalized to the joined form.
10064                        if self.check(TokenType::Integer) {
10065                            let n = self.consume(TokenType::Integer)?.value.clone();
10066                            let unit = self.consume_any_ident_or_kw()?.value.clone();
10067                            node.window = format!("{n}{unit}");
10068                        } else {
10069                            node.window = self.consume_any_ident_or_kw()?.value.clone();
10070                        }
10071                    }
10072                    "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
10073                    "effects" => node.effects = Some(self.parse_effect_row()?),
10074                    other => {
10075                        return Err(self.error(&format!(
10076                            "unknown notify field `{other}` in notify `{}` — expected \
10077                             `channel:` / `to:` / `template:` / `window:` / `provenance:` / \
10078                             `effects:`",
10079                            node.name
10080                        )))
10081                    }
10082                }
10083            }
10084        }
10085        self.consume(TokenType::RBrace)?;
10086        Ok(node)
10087    }
10088
10089    /// v2.66.0 — one-token lookahead helper for the `secret(` form.
10090    /// v2.69.0 — is the NEXT token an identifier? (`budget <Name> { … }` vs
10091    /// a bare `budget` used as an ordinary identifier.)
10092    fn peek_is_identifier(&self) -> bool {
10093        self.tokens
10094            .get(self.pos + 1)
10095            .map(|t| t.ttype == TokenType::Identifier)
10096            .unwrap_or(false)
10097    }
10098
10099    fn peek_is_lparen(&self) -> bool {
10100        self.tokens
10101            .get(self.pos + 1)
10102            .map(|t| t.ttype == TokenType::LParen)
10103            .unwrap_or(false)
10104    }
10105
10106    /// closed-catalog discipline); the operation vocabulary is the checker's job.
10107    fn parse_deliver(&mut self) -> Result<crate::ast::DeliverDefinition, ParseError> {
10108        let tok = self.consume(TokenType::Deliver)?;
10109        let name = self.consume(TokenType::Identifier)?.value;
10110        let mut node = crate::ast::DeliverDefinition {
10111            name,
10112            loc: Loc {
10113                line: tok.line,
10114                column: tok.column,
10115            },
10116            ..Default::default()
10117        };
10118        self.consume(TokenType::LBrace)?;
10119        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10120            let field = self.current().clone();
10121            let field_name = field.value.clone();
10122            self.advance();
10123            if self.check(TokenType::Colon) {
10124                self.advance();
10125                match field_name.as_str() {
10126                    "target" => node.target = self.consume_any_ident_or_kw()?.value,
10127                    "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
10128                    "secret" => node.secret = self.consume_any_ident_or_kw()?.value,
10129                    "effects" => node.effects = Some(self.parse_effect_row()?),
10130                    other => {
10131                        return Err(self.error(&format!(
10132                            "unknown deliver field `{other}` in deliver `{}` — expected \
10133                             `target:` / `provenance:` / `secret:` / `effects:`, or an operation \
10134                             block (`upsert_contact {{ … }}` / `create_deal {{ … }}` / \
10135                             `add_note {{ … }}`)",
10136                            node.name
10137                        )))
10138                    }
10139                }
10140            } else if self.check(TokenType::LBrace) {
10141                node.ops
10142                    .push(self.parse_deliver_op(field_name, field.line, field.column)?);
10143            } else {
10144                return Err(self.error(&format!(
10145                    "unexpected `{field_name}` in deliver `{}` body — expected a `field:` or an \
10146                     operation block `{field_name} {{ … }}`",
10147                    node.name
10148                )));
10149            }
10150            if self.check(TokenType::Comma) {
10151                self.advance();
10152            }
10153        }
10154        self.consume(TokenType::RBrace)?;
10155        Ok(node)
10156    }
10157
10158    /// v2.60.0 — parse a delivery operation block whose `kind` was already
10159    /// consumed: `{ (field: value)* }`. Flat (unlike a document block, an
10160    /// operation has no nested children) — each member must be a `field: value`.
10161    fn parse_deliver_op(
10162        &mut self,
10163        kind: String,
10164        line: u32,
10165        column: u32,
10166    ) -> Result<crate::ast::DeliverOp, ParseError> {
10167        let mut op = crate::ast::DeliverOp {
10168            kind,
10169            loc: Loc { line, column },
10170            ..Default::default()
10171        };
10172        self.consume(TokenType::LBrace)?;
10173        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10174            let name = self.consume_any_ident_or_kw()?.value;
10175            self.consume(TokenType::Colon).map_err(|_| {
10176                self.error(&format!(
10177                    "in deliver operation `{}`: `{name}` must be a `field: value` pair — an \
10178                     operation binds scalar fields, it takes no nested blocks",
10179                    op.kind
10180                ))
10181            })?;
10182            let value = self.parse_doc_scalar()?;
10183            op.fields.push((name, value));
10184            if self.check(TokenType::Comma) {
10185                self.advance();
10186            }
10187        }
10188        self.consume(TokenType::RBrace)?;
10189        Ok(op)
10190    }
10191
10192    /// v2.42.0 — parse `savant <Name> { domain:, cognition{…}, memory{…},
10193    /// budget{…}, mandate <M> {…} … }`. The block surface only; catalog +
10194    /// ref-resolution + budget/interruptibility binding is the v2.42.0 checker's
10195    /// job (the standing parse/check split). Unknown fields are a hard error
10196    ///: a savant governs an expensive autonomous process.
10197    fn parse_savant(&mut self) -> Result<SavantDefinition, ParseError> {
10198        let tok = self.consume(TokenType::Savant)?;
10199        let name = self.consume(TokenType::Identifier)?.value;
10200        let mut node = SavantDefinition {
10201            name,
10202            loc: Loc {
10203                line: tok.line,
10204                column: tok.column,
10205            },
10206            ..Default::default()
10207        };
10208        self.consume(TokenType::LBrace)?;
10209        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10210            let field = self.current().clone();
10211            let field_name = field.value.clone();
10212            self.advance();
10213            if self.check(TokenType::Colon) {
10214                self.advance();
10215                match field_name.as_str() {
10216                    "domain" => node.domain = self.consume(TokenType::StringLit)?.value,
10217                    other => {
10218                        return Err(self.error(&format!(
10219                            "unknown savant field `{other}` in savant `{}` — expected \
10220                             `domain:` or a `cognition` / `memory` / `budget` / `mandate` block",
10221                            node.name
10222                        )))
10223                    }
10224                }
10225            } else if field_name == "cognition" {
10226                node.cognition = Some(self.parse_savant_cognition(field.line, field.column)?);
10227            } else if field_name == "memory" {
10228                node.memory = Some(self.parse_savant_memory(field.line, field.column)?);
10229            } else if field_name == "budget" {
10230                node.budget = Some(self.parse_savant_budget(field.line, field.column)?);
10231            } else if field_name == "mandate" {
10232                node.mandates
10233                    .push(self.parse_savant_mandate(field.line, field.column)?);
10234            } else {
10235                return Err(self.error(&format!(
10236                    "unexpected `{field_name}` in savant `{}` body — expected `domain:` or a \
10237                     `cognition` / `memory` / `budget` / `mandate` block",
10238                    node.name
10239                )));
10240            }
10241            if self.check(TokenType::Comma) {
10242                self.advance();
10243            }
10244        }
10245        self.consume(TokenType::RBrace)?;
10246        Ok(node)
10247    }
10248
10249    /// v2.42.0 — the `cognition { depth:, entropic_threshold:, divergence: }`
10250    /// sub-block. Catalog validation of `depth`/`divergence` is v2.42.0.
10251    fn parse_savant_cognition(
10252        &mut self,
10253        line: u32,
10254        column: u32,
10255    ) -> Result<SavantCognition, ParseError> {
10256        self.consume(TokenType::LBrace)?;
10257        let mut node = SavantCognition {
10258            loc: Loc { line, column },
10259            ..Default::default()
10260        };
10261        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10262            let key = self.consume_any_ident_or_kw()?.value;
10263            self.consume(TokenType::Colon)?;
10264            match key.as_str() {
10265                "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
10266                "entropic_threshold" => node.entropic_threshold = self.parse_optional_float(),
10267                "divergence" => node.divergence = self.consume_any_ident_or_kw()?.value,
10268                other => {
10269                    return Err(self.error(&format!(
10270                        "unknown savant `cognition` field `{other}` — expected \
10271                         `depth` / `entropic_threshold` / `divergence`"
10272                    )))
10273                }
10274            }
10275            if self.check(TokenType::Comma) {
10276                self.advance();
10277            }
10278        }
10279        self.consume(TokenType::RBrace)?;
10280        Ok(node)
10281    }
10282
10283    /// v2.42.0 — the `memory { backend:, corpus_graph:, isolation_level: }`
10284    /// sub-block. `backend` is resolved to a declared `memory`/`corpus` in v2.42.0.
10285    fn parse_savant_memory(
10286        &mut self,
10287        line: u32,
10288        column: u32,
10289    ) -> Result<SavantMemory, ParseError> {
10290        self.consume(TokenType::LBrace)?;
10291        let mut node = SavantMemory {
10292            loc: Loc { line, column },
10293            ..Default::default()
10294        };
10295        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10296            let key = self.consume_any_ident_or_kw()?.value;
10297            self.consume(TokenType::Colon)?;
10298            match key.as_str() {
10299                "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
10300                "corpus_graph" => {
10301                    node.corpus_graph = self.consume_any_ident_or_kw()?.value == "true"
10302                }
10303                "isolation_level" => node.isolation_level = self.consume_any_ident_or_kw()?.value,
10304                other => {
10305                    return Err(self.error(&format!(
10306                        "unknown savant `memory` field `{other}` — expected \
10307                         `backend` / `corpus_graph` / `isolation_level`"
10308                    )))
10309                }
10310            }
10311            if self.check(TokenType::Comma) {
10312                self.advance();
10313            }
10314        }
10315        self.consume(TokenType::RBrace)?;
10316        Ok(node)
10317    }
10318
10319    /// v2.42.0 — the `budget { max_iterations:, max_tool_synth: }` sub-block.
10320    /// Bound to a v2.28.0 linear budget (`RateLease`) in v2.42.0.
10321    fn parse_savant_budget(
10322        &mut self,
10323        line: u32,
10324        column: u32,
10325    ) -> Result<SavantBudget, ParseError> {
10326        self.consume(TokenType::LBrace)?;
10327        let mut node = SavantBudget {
10328            loc: Loc { line, column },
10329            ..Default::default()
10330        };
10331        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10332            let key = self.consume_any_ident_or_kw()?.value;
10333            self.consume(TokenType::Colon)?;
10334            match key.as_str() {
10335                "max_iterations" => node.max_iterations = self.parse_optional_int(),
10336                "max_tool_synth" => node.max_tool_synth = self.parse_optional_int(),
10337                other => {
10338                    return Err(self.error(&format!(
10339                        "unknown savant `budget` field `{other}` — expected \
10340                         `max_iterations` / `max_tool_synth`"
10341                    )))
10342                }
10343            }
10344            if self.check(TokenType::Comma) {
10345                self.advance();
10346            }
10347        }
10348        self.consume(TokenType::RBrace)?;
10349        Ok(node)
10350    }
10351
10352    /// v2.42.0 — the `mandate <Name> { objective:, output: }` sub-block. The
10353    /// `mandate` keyword is already consumed by `parse_savant`.
10354    fn parse_savant_mandate(
10355        &mut self,
10356        line: u32,
10357        column: u32,
10358    ) -> Result<SavantMandate, ParseError> {
10359        let name = self.consume(TokenType::Identifier)?.value;
10360        let mut node = SavantMandate {
10361            name,
10362            loc: Loc { line, column },
10363            ..Default::default()
10364        };
10365        self.consume(TokenType::LBrace)?;
10366        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10367            let key = self.consume_any_ident_or_kw()?.value;
10368            self.consume(TokenType::Colon)?;
10369            match key.as_str() {
10370                "objective" => node.objective = self.consume(TokenType::StringLit)?.value,
10371                "output" => node.output_type = self.consume_any_ident_or_kw()?.value,
10372                other => {
10373                    return Err(self.error(&format!(
10374                        "unknown savant `mandate` field `{other}` — expected `objective` / `output`"
10375                    )))
10376                }
10377            }
10378            if self.check(TokenType::Comma) {
10379                self.advance();
10380            }
10381        }
10382        self.consume(TokenType::RBrace)?;
10383        Ok(node)
10384    }
10385
10386    /// v2.42.0 — parse `synth <Name> { target:, risk:, language:, sandbox:,
10387    /// review:, max_lines: }`. Flat key:value block (the `cache` shape). Catalog
10388    /// + deny-by-default validation is v2.42.0 `check_synth`. Unknown fields are a
10389    /// hard error: a synth policy governs arbitrary-code execution.
10390    fn parse_synth(&mut self) -> Result<SynthDefinition, ParseError> {
10391        let tok = self.consume(TokenType::Synth)?;
10392        let name = self.consume(TokenType::Identifier)?.value;
10393        let mut node = SynthDefinition {
10394            name,
10395            loc: Loc {
10396                line: tok.line,
10397                column: tok.column,
10398            },
10399            ..Default::default()
10400        };
10401        self.consume(TokenType::LBrace)?;
10402        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10403            let key = self.consume_any_ident_or_kw()?.value;
10404            self.consume(TokenType::Colon)?;
10405            match key.as_str() {
10406                "target" => node.target = self.consume(TokenType::StringLit)?.value,
10407                "risk" => node.risk = self.consume_any_ident_or_kw()?.value,
10408                "language" => node.language = self.consume_any_ident_or_kw()?.value,
10409                "sandbox" => node.sandbox = self.consume_any_ident_or_kw()?.value,
10410                "review" => node.review = self.consume_any_ident_or_kw()?.value,
10411                "max_lines" => node.max_lines = self.parse_optional_int(),
10412                other => {
10413                    return Err(self.error(&format!(
10414                        "unknown synth field `{other}` in synth `{}` — expected `target` / `risk` \
10415                         / `language` / `sandbox` / `review` / `max_lines`",
10416                        node.name
10417                    )))
10418                }
10419            }
10420            if self.check(TokenType::Comma) {
10421                self.consume(TokenType::Comma)?;
10422            }
10423        }
10424        self.consume(TokenType::RBrace)?;
10425        Ok(node)
10426    }
10427
10428    /// v2.37.0 — parse `voice Name { fields }`. Cross-field laws
10429    /// (stt/tts XOR realtime, interruptible ⇒ legal_basis, ref resolution)
10430    /// are v2.37.0 type-checker territory (T852), same parse/check split as
10431    /// every primitive in this file.
10432    fn parse_voice(&mut self) -> Result<VoiceDefinition, ParseError> {
10433        let tok = self.consume(TokenType::Voice)?;
10434        let name = self.consume(TokenType::Identifier)?.value;
10435        let mut node = VoiceDefinition {
10436            name,
10437            loc: Loc { line: tok.line, column: tok.column },
10438            ..Default::default()
10439        };
10440        self.consume(TokenType::LBrace)?;
10441        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10442            let key = self.consume_any_ident_or_kw()?.value;
10443            self.consume(TokenType::Colon)?;
10444            match key.as_str() {
10445                // Each leg: a declared upstream name or a `Preset@vN` ref.
10446                "stt" => node.stt = Some(self.parse_upstream_ref()?),
10447                "tts" => node.tts = Some(self.parse_upstream_ref()?),
10448                "realtime" => node.realtime = Some(self.parse_upstream_ref()?),
10449                "carrier" => node.carrier = self.consume_any_ident_or_kw()?.value,
10450                "interruptible" => {
10451                    let v = self.consume_any_ident_or_kw()?.value;
10452                    node.interruptible = v == "true";
10453                }
10454                "legal_basis" => node.legal_basis = Some(self.consume_any_ident_or_kw()?.value),
10455                "persona" => node.persona = Some(self.consume(TokenType::Identifier)?.value),
10456                "context" => node.context = Some(self.consume(TokenType::Identifier)?.value),
10457                other => return Err(self.error(&format!("unknown voice field `{other}`"))),
10458            }
10459            if self.check(TokenType::Comma) {
10460                self.consume(TokenType::Comma)?;
10461            }
10462        }
10463        self.consume(TokenType::RBrace)?;
10464        Ok(node)
10465    }
10466
10467    /// v2.37.0 — an upstream leg reference: `Ident` (a declared
10468    /// `upstream`) or `Ident@vN` (a v2.37.0 preset).
10469    fn parse_upstream_ref(&mut self) -> Result<String, ParseError> {
10470        let base = self.consume(TokenType::Identifier)?.value;
10471        if self.check(TokenType::At) {
10472            self.advance();
10473            let version = self.consume_any_ident_or_kw()?.value;
10474            Ok(format!("{base}@{version}"))
10475        } else {
10476            Ok(base)
10477        }
10478    }
10479
10480    /// v2.37.0 — parse the `map: [ rule, … ]` projection list.
10481    ///
10482    /// rule := (`send` | `receive`) <MessageType> `as` (`json` | `binary`)
10483    ///         [ `tag` <string> ]                 — send-json only
10484    ///         [ `when` <string> `=` <string> ]   — receive-json only
10485    fn parse_upstream_map(&mut self) -> Result<Vec<UpstreamMapRule>, ParseError> {
10486        self.consume(TokenType::LBracket)?;
10487        let mut rules = Vec::new();
10488        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
10489            let dir_tok = self.current().clone();
10490            let direction = match dir_tok.ttype {
10491                TokenType::Send => "send",
10492                TokenType::Receive => "receive",
10493                _ => {
10494                    return Err(self.error(&format!(
10495                        "upstream map rule must start with `send` or `receive`, got `{}`",
10496                        dir_tok.value
10497                    )))
10498                }
10499            };
10500            self.advance();
10501            let message = self.consume(TokenType::Identifier)?.value;
10502            self.consume(TokenType::As)?;
10503            let framing = self.consume_any_ident_or_kw()?.value;
10504            let mut rule = UpstreamMapRule {
10505                direction: direction.to_string(),
10506                message,
10507                framing,
10508                loc: Loc { line: dir_tok.line, column: dir_tok.column },
10509                ..Default::default()
10510            };
10511            // Optional selectors — contextual identifiers, not keywords.
10512            if self.current().value == "tag" {
10513                self.advance();
10514                rule.tag = Some(self.consume(TokenType::StringLit)?.value);
10515            } else if self.current().value == "when" {
10516                // `when "f" = "v"` — equality discriminator; `when "f"` —
10517                // field-PRESENCE discriminator (vendors like Gemini Live /
10518                // ElevenLabs mark frame kinds by which key exists, not by a
10519                // type value).
10520                self.advance();
10521                rule.when_field = Some(self.consume(TokenType::StringLit)?.value);
10522                if self.check(TokenType::Assign) {
10523                    self.advance();
10524                    rule.when_value = Some(self.consume(TokenType::StringLit)?.value);
10525                }
10526            }
10527            rules.push(rule);
10528            if self.check(TokenType::Comma) {
10529                self.advance();
10530            }
10531        }
10532        self.consume(TokenType::RBracket)?;
10533        Ok(rules)
10534    }
10535
10536    /// v2.37.0 — parse `reconnect: { backoff_ms: <int>, max_attempts:
10537    /// <int>, on_exhausted: <ident> }` (order-free, all three required —
10538    /// a reconnection policy with a hole is not a policy).
10539    fn parse_upstream_reconnect(&mut self) -> Result<UpstreamReconnect, ParseError> {
10540        self.consume(TokenType::LBrace)?;
10541        let mut backoff_ms: Option<i64> = None;
10542        let mut max_attempts: Option<i64> = None;
10543        let mut on_exhausted: Option<String> = None;
10544        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10545            let key = self.consume_any_ident_or_kw()?.value;
10546            self.consume(TokenType::Colon)?;
10547            match key.as_str() {
10548                "backoff_ms" => {
10549                    backoff_ms = Some(
10550                        self.consume(TokenType::Integer)?
10551                            .value
10552                            .parse::<i64>()
10553                            .map_err(|_| self.error("backoff_ms must be an integer"))?,
10554                    )
10555                }
10556                "max_attempts" => {
10557                    max_attempts = Some(
10558                        self.consume(TokenType::Integer)?
10559                            .value
10560                            .parse::<i64>()
10561                            .map_err(|_| self.error("max_attempts must be an integer"))?,
10562                    )
10563                }
10564                "on_exhausted" => on_exhausted = Some(self.consume_any_ident_or_kw()?.value),
10565                other => return Err(self.error(&format!("unknown reconnect field `{other}`"))),
10566            }
10567            if self.check(TokenType::Comma) {
10568                self.consume(TokenType::Comma)?;
10569            }
10570        }
10571        self.consume(TokenType::RBrace)?;
10572        match (backoff_ms, max_attempts, on_exhausted) {
10573            (Some(b), Some(m), Some(o)) => Ok(UpstreamReconnect { backoff_ms: b, max_attempts: m, on_exhausted: o }),
10574            _ => Err(self.error(
10575                "reconnect requires all of `backoff_ms:`, `max_attempts:`, `on_exhausted:` — a reconnection policy with a hole is not a policy",
10576            )),
10577        }
10578    }
10579
10580    /// Parse: `[send T, receive U, loop, end]`.
10581    fn parse_session_steps(&mut self) -> Result<Vec<SessionStep>, ParseError> {
10582        self.consume(TokenType::LBracket)?;
10583        let mut steps = Vec::new();
10584        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
10585            steps.push(self.parse_session_step()?);
10586            if self.check(TokenType::Comma) {
10587                self.advance();
10588            }
10589        }
10590        self.consume(TokenType::RBracket)?;
10591        Ok(steps)
10592    }
10593
10594    /// v2.36.0 — a **brace**-delimited session step block: `{ step, step, … }`.
10595    /// Used by the `interrupt`/`resumable` regions (the paper's block surface),
10596    /// as opposed to the `[ … ]` step-lists used by roles and choice arms.
10597    fn parse_session_step_block(&mut self) -> Result<Vec<SessionStep>, ParseError> {
10598        self.consume(TokenType::LBrace)?;
10599        let mut steps = Vec::new();
10600        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10601            steps.push(self.parse_session_step()?);
10602            if self.check(TokenType::Comma) {
10603                self.advance();
10604            }
10605        }
10606        self.consume(TokenType::RBrace)?;
10607        Ok(steps)
10608    }
10609
10610    fn parse_session_step(&mut self) -> Result<SessionStep, ParseError> {
10611        let tok = self.current().clone();
10612        let loc = Loc { line: tok.line, column: tok.column };
10613        match tok.ttype {
10614            TokenType::Send => {
10615                self.advance();
10616                let msg = self.consume_any_ident_or_kw()?;
10617                Ok(SessionStep { op: "send".into(), message_type: msg.value, loc, ..Default::default() })
10618            }
10619            TokenType::Receive => {
10620                self.advance();
10621                let msg = self.consume_any_ident_or_kw()?;
10622                Ok(SessionStep { op: "receive".into(), message_type: msg.value, loc, ..Default::default() })
10623            }
10624            TokenType::Loop => {
10625                self.advance();
10626                Ok(SessionStep { op: "loop".into(), loc, ..Default::default() })
10627            }
10628            TokenType::End => {
10629                self.advance();
10630                Ok(SessionStep { op: "end".into(), loc, ..Default::default() })
10631            }
10632            // v2.3.0 — choice: `select { ℓ: [..], … }` (⊕) | `branch { ℓ: [..], … }` (&).
10633            // `select`/`branch` are not keywords — they arrive as identifiers.
10634            TokenType::Identifier if tok.value == "select" || tok.value == "branch" => {
10635                self.parse_session_choice(&tok.value, loc)
10636            }
10637            // v2.36.0 — `interrupt { <body> } on <Signal> as <sig> resumable { <handler> }`.
10638            // Contextual keyword (identifier), like `select`/`branch`.
10639            TokenType::Identifier if tok.value == "interrupt" => {
10640                self.parse_session_interrupt(loc)
10641            }
10642            // v2.36.0 — `resume`: the handler's normal exit (hand control back to
10643            // the parked body). A bare step, no payload; only meaningful inside an
10644            // `interrupt` handler (enforced at type-check, v2.36.0).
10645            //
10646            // ⚠️ v2.87.0 — this guard used to require `TokenType::Identifier`,
10647            // and `resume` became a HARD KEYWORD when the algebraic-effect
10648            // constructs landed. The session `resume` is a DIFFERENT `resume`
10649            // (v2.36.0's interrupt-handler exit, not v2.87.0's one-shot continuation
10650            // invocation), and it broke the moment the lexer stopped handing it
10651            // over as an identifier — `axon-frontend/src/voice_desugar.rs`'s own
10652            // expansion source stopped parsing.
10653            //
10654            // Matching on the VALUE rather than the token type is what keeps a
10655            // contextual keyword contextual. This was caught by the corpus gate
10656            // (`effect_grammar::a7_…`), not by review: six new hard
10657            // keywords across a 106-file `.axon` corpus is not a risk anyone
10658            // eyeballs correctly.
10659            _ if tok.value == "resume" => {
10660                self.advance();
10661                Ok(SessionStep { op: "resume".into(), loc, ..Default::default() })
10662            }
10663            _ => Err(ParseError {
10664                message: format!(
10665                    "Invalid session step '{}' — expected send | receive | loop | end | select | branch | interrupt | resume",
10666                    tok.value
10667                ),
10668                line: tok.line,
10669                column: tok.column,
10670                ..Default::default()
10671            }),
10672        }
10673    }
10674
10675    /// v2.36.0 — consume a **contextual keyword** (`on` / `as` / `resumable`):
10676    /// a token whose *value* must equal `kw`, regardless of whether the lexer
10677    /// classified it as a keyword or a bare identifier. Keeps the `interrupt`
10678    /// surface readable without minting three reserved words.
10679    fn consume_contextual(&mut self, kw: &str) -> Result<(), ParseError> {
10680        let t = self.current().clone();
10681        if t.value != kw {
10682            return Err(ParseError {
10683                message: format!("expected `{kw}` in interrupt step, got `{}`", t.value),
10684                line: t.line,
10685                column: t.column,
10686                ..Default::default()
10687            });
10688        }
10689        self.advance();
10690        Ok(())
10691    }
10692
10693    /// v2.36.0 — Parse an interruptible region:
10694    /// `interrupt { <body-steps> } on <Signal> as <sig> resumable { <handler-steps> }`.
10695    ///
10696    /// Encoded into the string-tagged `SessionStep` (mirroring the v2.3.0 choice
10697    /// shape): `op = "interrupt"`, `message_type = <Signal>` (validated against the
10698    /// closed `CallInterruptCause` catalog at type-check, v2.36.0), two labelled
10699    /// `branches` (`body`, `handler`), `binder = <sig>`, `resumable = true`.
10700    fn parse_session_interrupt(&mut self, loc: Loc) -> Result<SessionStep, ParseError> {
10701        self.advance(); // consume `interrupt`
10702        // Body region — a brace-delimited step block (the paper's `interrupt { … }`
10703        // surface; distinct from the `[ … ]` step-lists of roles/choice arms).
10704        let body = self.parse_session_step_block()?;
10705        // `on <Signal>`
10706        self.consume_contextual("on")?;
10707        let signal = self.consume_any_ident_or_kw()?;
10708        // `as <sig>`
10709        self.consume_contextual("as")?;
10710        let binder = self.consume_any_ident_or_kw()?;
10711        // `resumable { <handler> }`
10712        self.consume_contextual("resumable")?;
10713        let handler = self.parse_session_step_block()?;
10714        Ok(SessionStep {
10715            op: "interrupt".into(),
10716            message_type: signal.value,
10717            branches: vec![
10718                SessionBranch { label: "body".into(), steps: body, loc: loc.clone() },
10719                SessionBranch { label: "handler".into(), steps: handler, loc: loc.clone() },
10720            ],
10721            binder: binder.value,
10722            resumable: true,
10723            loc,
10724        })
10725    }
10726
10727    /// v2.3.0 — Parse a choice step: `select { ask: [..], cancel: [..] }`
10728    /// (or `branch { … }`). Each `label: [steps]` arm is a nested sub-protocol.
10729    fn parse_session_choice(&mut self, op: &str, loc: Loc) -> Result<SessionStep, ParseError> {
10730        self.advance(); // consume `select` / `branch`
10731        self.consume(TokenType::LBrace)?;
10732        let mut branches = Vec::new();
10733        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10734            let label_tok = self.consume_any_ident_or_kw()?;
10735            self.consume(TokenType::Colon)?;
10736            let steps = self.parse_session_steps()?;
10737            branches.push(SessionBranch {
10738                label: label_tok.value,
10739                steps,
10740                loc: Loc { line: label_tok.line, column: label_tok.column },
10741            });
10742            if self.check(TokenType::Comma) {
10743                self.advance();
10744            }
10745        }
10746        self.consume(TokenType::RBrace)?;
10747        Ok(SessionStep { op: op.to_string(), branches, loc, ..Default::default() })
10748    }
10749
10750    /// Parse: `topology Name { nodes: [A, B, …]  edges: [A -> B : Session, …] }`.
10751    fn parse_topology(&mut self) -> Result<TopologyDefinition, ParseError> {
10752        let tok = self.consume(TokenType::Topology)?;
10753        let name = self.consume(TokenType::Identifier)?.value;
10754        let mut node = TopologyDefinition {
10755            name,
10756            nodes: Vec::new(),
10757            edges: Vec::new(),
10758            loc: Loc {
10759                line: tok.line,
10760                column: tok.column,
10761            },
10762            leading_trivia: Vec::new(),
10763            trailing_trivia: Vec::new(),
10764        };
10765        self.consume(TokenType::LBrace)?;
10766        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10767            let field_name = self.current().value.clone();
10768            self.advance();
10769            if !self.check(TokenType::Colon) {
10770                if self.check(TokenType::LBrace) {
10771                    self.skip_braced_block()?;
10772                }
10773                continue;
10774            }
10775            self.advance();
10776            match field_name.as_str() {
10777                "nodes" => node.nodes = self.parse_bracketed_identifiers()?,
10778                "edges" => node.edges = self.parse_topology_edges()?,
10779                _ => self.skip_value(),
10780            }
10781        }
10782        self.consume(TokenType::RBrace)?;
10783        Ok(node)
10784    }
10785
10786    fn parse_topology_edges(&mut self) -> Result<Vec<TopologyEdge>, ParseError> {
10787        self.consume(TokenType::LBracket)?;
10788        let mut edges = Vec::new();
10789        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
10790            edges.push(self.parse_topology_edge()?);
10791            if self.check(TokenType::Comma) {
10792                self.advance();
10793            }
10794        }
10795        self.consume(TokenType::RBracket)?;
10796        Ok(edges)
10797    }
10798
10799    fn parse_topology_edge(&mut self) -> Result<TopologyEdge, ParseError> {
10800        let src_tok = self.consume_any_ident_or_kw()?;
10801        self.consume(TokenType::Arrow)?;
10802        let tgt_tok = self.consume_any_ident_or_kw()?;
10803        self.consume(TokenType::Colon)?;
10804        let sess_tok = self.consume_any_ident_or_kw()?;
10805        Ok(TopologyEdge {
10806            source: src_tok.value,
10807            target: tgt_tok.value,
10808            session_ref: sess_tok.value,
10809            loc: Loc {
10810                line: src_tok.line,
10811                column: src_tok.column,
10812            },
10813        })
10814    }
10815
10816    // ── v1.1.0 — Cognitive immune system (paper_immune_v2.md) ────
10817
10818    /// Parse: `immune Name { watch, sensitivity, baseline, window, scope, tau, decay }`.
10819    fn parse_immune(&mut self) -> Result<ImmuneDefinition, ParseError> {
10820        let tok = self.consume(TokenType::Immune)?;
10821        let name = self.consume(TokenType::Identifier)?.value;
10822        let mut node = ImmuneDefinition {
10823            name,
10824            watch: Vec::new(),
10825            sensitivity: None,
10826            baseline: "learned".to_string(),
10827            window: 100,
10828            scope: String::new(),
10829            tau: String::new(),
10830            decay: "exponential".to_string(),
10831            loc: Loc {
10832                line: tok.line,
10833                column: tok.column,
10834            },
10835            leading_trivia: Vec::new(),
10836            trailing_trivia: Vec::new(),
10837        };
10838        self.consume(TokenType::LBrace)?;
10839        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10840            let field_name = self.current().value.clone();
10841            self.advance();
10842            if !self.check(TokenType::Colon) {
10843                if self.check(TokenType::LBrace) {
10844                    self.skip_braced_block()?;
10845                }
10846                continue;
10847            }
10848            self.advance();
10849            match field_name.as_str() {
10850                "watch" => node.watch = self.parse_bracketed_identifiers()?,
10851                "sensitivity" => node.sensitivity = self.parse_optional_float(),
10852                "baseline" => node.baseline = self.consume_any_ident_or_kw()?.value,
10853                "window" => {
10854                    if let Some(v) = self.parse_optional_int() {
10855                        node.window = v;
10856                    }
10857                }
10858                "scope" => {
10859                    let s_tok = self.consume_any_ident_or_kw()?;
10860                    let s = s_tok.value;
10861                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
10862                        return Err(ParseError {
10863                            message: format!(
10864                                "Invalid scope '{s}' in immune '{}' — \
10865                                 expected tenant | flow | global",
10866                                node.name
10867                            ),
10868                            line: s_tok.line,
10869                            column: s_tok.column,
10870                                                    ..Default::default()
10871                        });
10872                    }
10873                    node.scope = s;
10874                }
10875                "tau" => {
10876                    let t = self.current().clone();
10877                    match t.ttype {
10878                        TokenType::Duration | TokenType::StringLit => {
10879                            self.advance();
10880                            node.tau = t.value;
10881                        }
10882                        _ => node.tau = self.consume_any_ident_or_kw()?.value,
10883                    }
10884                }
10885                "decay" => {
10886                    let d_tok = self.consume_any_ident_or_kw()?;
10887                    let d = d_tok.value;
10888                    if !matches!(d.as_str(), "exponential" | "linear" | "none") {
10889                        return Err(ParseError {
10890                            message: format!(
10891                                "Invalid decay '{d}' in immune '{}' — \
10892                                 expected exponential | linear | none",
10893                                node.name
10894                            ),
10895                            line: d_tok.line,
10896                            column: d_tok.column,
10897                                                    ..Default::default()
10898                        });
10899                    }
10900                    node.decay = d;
10901                }
10902                _ => self.skip_value(),
10903            }
10904        }
10905        self.consume(TokenType::RBrace)?;
10906        Ok(node)
10907    }
10908
10909    /// Parse: `reflex Name { trigger, on_level, action, scope, sla }`.
10910    fn parse_reflex(&mut self) -> Result<ReflexDefinition, ParseError> {
10911        let tok = self.consume(TokenType::Reflex)?;
10912        let name = self.consume(TokenType::Identifier)?.value;
10913        let mut node = ReflexDefinition {
10914            name,
10915            trigger: String::new(),
10916            on_level: "doubt".to_string(),
10917            action: String::new(),
10918            scope: String::new(),
10919            sla: String::new(),
10920            loc: Loc {
10921                line: tok.line,
10922                column: tok.column,
10923            },
10924            leading_trivia: Vec::new(),
10925            trailing_trivia: Vec::new(),
10926        };
10927        self.consume(TokenType::LBrace)?;
10928        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10929            let field_name = self.current().value.clone();
10930            self.advance();
10931            if !self.check(TokenType::Colon) {
10932                if self.check(TokenType::LBrace) {
10933                    self.skip_braced_block()?;
10934                }
10935                continue;
10936            }
10937            self.advance();
10938            match field_name.as_str() {
10939                "trigger" => node.trigger = self.consume_any_ident_or_kw()?.value,
10940                "on_level" => {
10941                    let l_tok = self.consume_any_ident_or_kw()?;
10942                    let l = l_tok.value;
10943                    if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
10944                        return Err(ParseError {
10945                            message: format!(
10946                                "Invalid on_level '{l}' in reflex '{}' — \
10947                                 expected know | believe | speculate | doubt",
10948                                node.name
10949                            ),
10950                            line: l_tok.line,
10951                            column: l_tok.column,
10952                                                    ..Default::default()
10953                        });
10954                    }
10955                    node.on_level = l;
10956                }
10957                "action" => {
10958                    let a_tok = self.consume_any_ident_or_kw()?;
10959                    let a = a_tok.value;
10960                    if !matches!(
10961                        a.as_str(),
10962                        "drop"
10963                            | "revoke"
10964                            | "emit"
10965                            | "redact"
10966                            | "quarantine"
10967                            | "terminate"
10968                            | "alert"
10969                    ) {
10970                        return Err(ParseError {
10971                            message: format!(
10972                                "Invalid action '{a}' in reflex '{}' — \
10973                                 expected drop | revoke | emit | redact | \
10974                                 quarantine | terminate | alert",
10975                                node.name
10976                            ),
10977                            line: a_tok.line,
10978                            column: a_tok.column,
10979                                                    ..Default::default()
10980                        });
10981                    }
10982                    node.action = a;
10983                }
10984                "scope" => {
10985                    let s_tok = self.consume_any_ident_or_kw()?;
10986                    let s = s_tok.value;
10987                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
10988                        return Err(ParseError {
10989                            message: format!(
10990                                "Invalid scope '{s}' in reflex '{}' — \
10991                                 expected tenant | flow | global",
10992                                node.name
10993                            ),
10994                            line: s_tok.line,
10995                            column: s_tok.column,
10996                                                    ..Default::default()
10997                        });
10998                    }
10999                    node.scope = s;
11000                }
11001                "sla" => {
11002                    let t = self.current().clone();
11003                    match t.ttype {
11004                        TokenType::Duration | TokenType::StringLit => {
11005                            self.advance();
11006                            node.sla = t.value;
11007                        }
11008                        _ => node.sla = self.consume_any_ident_or_kw()?.value,
11009                    }
11010                }
11011                _ => self.skip_value(),
11012            }
11013        }
11014        self.consume(TokenType::RBrace)?;
11015        Ok(node)
11016    }
11017
11018    /// Parse: `heal Name { source, on_level, mode, scope, review_sla, shield, max_patches }`.
11019    fn parse_heal(&mut self) -> Result<HealDefinition, ParseError> {
11020        let tok = self.consume(TokenType::Heal)?;
11021        let name = self.consume(TokenType::Identifier)?.value;
11022        let mut node = HealDefinition {
11023            name,
11024            source: String::new(),
11025            on_level: "doubt".to_string(),
11026            mode: "human_in_loop".to_string(),
11027            scope: String::new(),
11028            review_sla: String::new(),
11029            shield_ref: String::new(),
11030            max_patches: 3,
11031            loc: Loc {
11032                line: tok.line,
11033                column: tok.column,
11034            },
11035            leading_trivia: Vec::new(),
11036            trailing_trivia: Vec::new(),
11037        };
11038        self.consume(TokenType::LBrace)?;
11039        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11040            let field_name = self.current().value.clone();
11041            self.advance();
11042            if !self.check(TokenType::Colon) {
11043                if self.check(TokenType::LBrace) {
11044                    self.skip_braced_block()?;
11045                }
11046                continue;
11047            }
11048            self.advance();
11049            match field_name.as_str() {
11050                "source" => node.source = self.consume_any_ident_or_kw()?.value,
11051                "on_level" => {
11052                    let l_tok = self.consume_any_ident_or_kw()?;
11053                    let l = l_tok.value;
11054                    if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
11055                        return Err(ParseError {
11056                            message: format!(
11057                                "Invalid on_level '{l}' in heal '{}' — \
11058                                 expected know | believe | speculate | doubt",
11059                                node.name
11060                            ),
11061                            line: l_tok.line,
11062                            column: l_tok.column,
11063                                                    ..Default::default()
11064                        });
11065                    }
11066                    node.on_level = l;
11067                }
11068                "mode" => {
11069                    let m_tok = self.consume_any_ident_or_kw()?;
11070                    let m = m_tok.value;
11071                    if !matches!(m.as_str(), "audit_only" | "human_in_loop" | "adversarial") {
11072                        return Err(ParseError {
11073                            message: format!(
11074                                "Invalid mode '{m}' in heal '{}' — \
11075                                 expected audit_only | human_in_loop | adversarial",
11076                                node.name
11077                            ),
11078                            line: m_tok.line,
11079                            column: m_tok.column,
11080                                                    ..Default::default()
11081                        });
11082                    }
11083                    node.mode = m;
11084                }
11085                "scope" => {
11086                    let s_tok = self.consume_any_ident_or_kw()?;
11087                    let s = s_tok.value;
11088                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
11089                        return Err(ParseError {
11090                            message: format!(
11091                                "Invalid scope '{s}' in heal '{}' — \
11092                                 expected tenant | flow | global",
11093                                node.name
11094                            ),
11095                            line: s_tok.line,
11096                            column: s_tok.column,
11097                                                    ..Default::default()
11098                        });
11099                    }
11100                    node.scope = s;
11101                }
11102                "review_sla" => {
11103                    let t = self.current().clone();
11104                    match t.ttype {
11105                        TokenType::Duration | TokenType::StringLit => {
11106                            self.advance();
11107                            node.review_sla = t.value;
11108                        }
11109                        _ => node.review_sla = self.consume_any_ident_or_kw()?.value,
11110                    }
11111                }
11112                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
11113                "max_patches" => {
11114                    if let Some(v) = self.parse_optional_int() {
11115                        node.max_patches = v;
11116                    }
11117                }
11118                _ => self.skip_value(),
11119            }
11120        }
11121        self.consume(TokenType::RBrace)?;
11122        Ok(node)
11123    }
11124
11125    // ── v1.3.1 — UI cognitiva (component / view) ────────────
11126
11127    /// Parse: `component Name { renders, via_shield, on_interact, render_hint }`.
11128    fn parse_component(&mut self) -> Result<ComponentDefinition, ParseError> {
11129        let tok = self.consume(TokenType::Component)?;
11130        let name = self.consume(TokenType::Identifier)?.value;
11131        let mut node = ComponentDefinition {
11132            name,
11133            renders: String::new(),
11134            via_shield: String::new(),
11135            on_interact: String::new(),
11136            render_hint: "custom".to_string(),
11137            loc: Loc {
11138                line: tok.line,
11139                column: tok.column,
11140            },
11141            leading_trivia: Vec::new(),
11142            trailing_trivia: Vec::new(),
11143        };
11144        self.consume(TokenType::LBrace)?;
11145        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11146            let field_name = self.current().value.clone();
11147            self.advance();
11148            if !self.check(TokenType::Colon) {
11149                if self.check(TokenType::LBrace) {
11150                    self.skip_braced_block()?;
11151                }
11152                continue;
11153            }
11154            self.advance();
11155            match field_name.as_str() {
11156                "renders" => node.renders = self.consume_any_ident_or_kw()?.value,
11157                "via_shield" => node.via_shield = self.consume_any_ident_or_kw()?.value,
11158                "on_interact" => node.on_interact = self.consume_any_ident_or_kw()?.value,
11159                "render_hint" => {
11160                    let h_tok = self.consume_any_ident_or_kw()?;
11161                    let h = h_tok.value;
11162                    if !matches!(h.as_str(), "card" | "list" | "form" | "chart" | "custom") {
11163                        return Err(ParseError {
11164                            message: format!(
11165                                "Invalid render_hint '{h}' in component '{}' — \
11166                                 expected card | list | form | chart | custom",
11167                                node.name
11168                            ),
11169                            line: h_tok.line,
11170                            column: h_tok.column,
11171                                                    ..Default::default()
11172                        });
11173                    }
11174                    node.render_hint = h;
11175                }
11176                _ => self.skip_value(),
11177            }
11178        }
11179        self.consume(TokenType::RBrace)?;
11180        Ok(node)
11181    }
11182
11183    /// Parse: `view Name { title, components: [...], route }`.
11184    fn parse_view(&mut self) -> Result<ViewDefinition, ParseError> {
11185        let tok = self.consume(TokenType::View)?;
11186        let name = self.consume(TokenType::Identifier)?.value;
11187        let mut node = ViewDefinition {
11188            name,
11189            title: String::new(),
11190            components: Vec::new(),
11191            route: String::new(),
11192            loc: Loc {
11193                line: tok.line,
11194                column: tok.column,
11195            },
11196            leading_trivia: Vec::new(),
11197            trailing_trivia: Vec::new(),
11198        };
11199        self.consume(TokenType::LBrace)?;
11200        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11201            let field_name = self.current().value.clone();
11202            self.advance();
11203            if !self.check(TokenType::Colon) {
11204                if self.check(TokenType::LBrace) {
11205                    self.skip_braced_block()?;
11206                }
11207                continue;
11208            }
11209            self.advance();
11210            match field_name.as_str() {
11211                "title" => node.title = self.consume(TokenType::StringLit)?.value,
11212                "components" => node.components = self.parse_bracketed_identifiers()?,
11213                "route" => node.route = self.consume(TokenType::StringLit)?.value,
11214                _ => self.skip_value(),
11215            }
11216        }
11217        self.consume(TokenType::RBrace)?;
11218        Ok(node)
11219    }
11220
11221    fn parse_axonendpoint(&mut self) -> Result<AxonEndpointDefinition, ParseError> {
11222        let tok = self.consume(TokenType::AxonEndpoint)?;
11223        let name = self.consume(TokenType::Identifier)?.value;
11224        let mut node = AxonEndpointDefinition {
11225            name,
11226            method: String::new(),
11227            path: String::new(),
11228            body_type: String::new(),
11229            execute_flow: String::new(),
11230            output_type: String::new(),
11231            shield_ref: String::new(),
11232            // v2.38.0 — `cors:` reference; empty ≡ no cors declared
11233            // (the design decision: no CORS headers, ever — secure by default).
11234            cors_ref: String::new(),
11235            retries: None,
11236            timeout: String::new(),
11237            compliance: Vec::new(),
11238            // v1.21.0 — Defaults preserve backwards compat per D1.
11239            transport: "json".to_string(),
11240            keepalive: String::new(),
11241            // v1.22.0 — Inference fields (parser-default state).
11242            // Both fields toggle/populate only when the source provides
11243            // an explicit `transport:` declaration (parser sets
11244            // `transport_explicit = true`) AND the type-checker walks
11245            // the program to compute `implicit_transport`.
11246            transport_explicit: false,
11247            implicit_transport: String::new(),
11248            // v1.23.0 (D8) — auth scope; empty list ≡ no auth gate.
11249            requires_capabilities: Vec::new(),
11250            // v2.44.0 — explicit authorization-coverage opt-out. Default
11251            // false; the v2.44.0 rule requires coverage OR `public: true`.
11252            public: false,
11253            // v1.23.0 — Replay-token binding (D9 plan-vivo).
11254            // Parser defaults: not explicit; effective value resolved
11255            // at deploy time using the method-default heuristic.
11256            replay_explicit: false,
11257            replay: false,
11258            // v1.28.0 — Wire-format dialect default
11259            // empty; the runtime classifier resolves the default
11260            // dialect per the algebraic-effect predicate when the
11261            // source omits `transport: sse(<dialect>)`.
11262            transport_dialect: String::new(),
11263            // v1.27.1 — Algebraic-effect override.
11264            // Parser default false; populated by the type-checker's
11265            // compute_implicit_transports pass once the full program
11266            // is known (the predicate cross-references tool effects
11267            // declared anywhere in the program).
11268            has_algebraic_stream_effect: false,
11269            // v1.31.0 (D2) — declared execution backend; empty ≡
11270            // not declared (the endpoint resolves down the v1.31.0 D1
11271            // ladder). A non-empty value is validated against the
11272            // closed `AXONENDPOINT_BACKEND_VALUES` catalog below.
11273            backend: String::new(),
11274            // v1.32.0 (D1) — Path-param names extracted from the
11275            // `path:` string AFTER the field is parsed. Initialized
11276            // empty; populated by `extract_path_param_names` after
11277            // the `path:` field is read in the loop below.
11278            path_params: Vec::new(),
11279            // v1.32.0 (D2) — Inline `query: { name: Type, name: Type? }`
11280            // block. Initialized empty; populated by the `"query"` arm
11281            // in the field loop below. Closed catalog enforced at parse
11282            // time per `axonendpoint_is_valid_query_param_type`.
11283            query_params: Vec::new(),
11284            loc: Loc {
11285                line: tok.line,
11286                column: tok.column,
11287            },
11288            leading_trivia: Vec::new(),
11289            trailing_trivia: Vec::new(),
11290        };
11291        self.consume(TokenType::LBrace)?;
11292        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11293            let field_name = self.current().value.clone();
11294            self.advance();
11295            if self.check(TokenType::Colon) {
11296                self.advance();
11297                match field_name.as_str() {
11298                    "method" => {
11299                        // v1.23.0 D3 — closed method enum
11300                        // `{GET, POST, PUT, DELETE, PATCH}`. Unknown
11301                        // values rejected at parse time with smart-
11302                        // suggest hint (v1.20.0). HEAD/OPTIONS/etc.
11303                        // are runtime-managed and not adopter-
11304                        // declarable.
11305                        let value_tok = self.consume_any_ident_or_kw()?;
11306                        let value_upper = value_tok.value.to_uppercase();
11307                        if !axonendpoint_is_valid_method(&value_upper) {
11308                            let hint = crate::smart_suggest::suggest_for(
11309                                &value_upper,
11310                                AXONENDPOINT_METHOD_VALUES,
11311                            );
11312                            let base = format!(
11313                                "Invalid method '{}' in axonendpoint '{}'.",
11314                                value_tok.value, node.name
11315                            );
11316                            let message = if hint.is_empty() {
11317                                format!(
11318                                    "{base} expected GET | POST | PUT | DELETE | PATCH, found {}",
11319                                    value_tok.value
11320                                )
11321                            } else {
11322                                format!(
11323                                    "{base} {hint} (expected GET | POST | PUT | DELETE | PATCH, found {})",
11324                                    value_tok.value
11325                                )
11326                            };
11327                            return Err(ParseError {
11328                                message,
11329                                line: value_tok.line,
11330                                column: value_tok.column,
11331                                ..Default::default()
11332                            });
11333                        }
11334                        node.method = value_upper;
11335                    }
11336                    "path" => {
11337                        node.path = self.consume(TokenType::StringLit)?.value.clone();
11338                        // v1.32.0 (D1) — extract `{name}` placeholders
11339                        // for the Request Binding Contract's path-param
11340                        // source. Duplicate `{name}` in the same path
11341                        // is rejected at parse time (HTTP route patterns
11342                        // structurally reject duplicates; surfacing the
11343                        // error here is friendlier than letting axum
11344                        // panic at registration).
11345                        match extract_path_param_names(&node.path) {
11346                            Ok(names) => node.path_params = names,
11347                            Err(dup) => {
11348                                let cur = self.current().clone();
11349                                return Err(ParseError {
11350                                    message: format!(
11351                                        "axonendpoint '{}' declares path '{}' \
11352                                         containing duplicate placeholder '{{{}}}'. \
11353                                         Each `{{name}}` in a `path:` must be \
11354                                         unique — the runtime cannot bind two \
11355                                         path segments to the same name.",
11356                                        node.name, node.path, dup,
11357                                    ),
11358                                    line: cur.line,
11359                                    column: cur.column,
11360                                    ..Default::default()
11361                                });
11362                            }
11363                        }
11364                    },
11365                    "body" => node.body_type = self.consume_any_ident_or_kw()?.value.clone(),
11366                    "query" => {
11367                        // v1.32.0 (D2) — Inline query-parameter block.
11368                        // Grammar: `query: { name: Type [, name: Type?]* }`.
11369                        // Closed type catalog
11370                        // `AXONENDPOINT_QUERY_PARAM_TYPES = {Text, Int,
11371                        // Float, Bool, Uuid}`. Optional via `?` suffix
11372                        // reuses `TypeExpr.optional` semantics already in
11373                        // use for flow parameters + body type fields. A
11374                        // duplicate field name in the same block is a
11375                        // parse error (HTTP query strings DO allow
11376                        // multi-value but v1.38.5 binds the first value
11377                        // only — see plan vivo section 7 forward-compat).
11378                        //
11379                        // v1.32.0 (D2 robustness) — declaring `query:`
11380                        // twice on the same axonendpoint silently merged
11381                        // params pre-hardening. Now it's a parse error
11382                        // so an adopter typo / copy-paste mistake
11383                        // surfaces with line + column instead of
11384                        // producing an unexpectedly-augmented endpoint.
11385                        let lbrace_tok = self.consume(TokenType::LBrace)?;
11386                        let block_line = lbrace_tok.line;
11387                        if !node.query_params.is_empty() {
11388                            return Err(ParseError {
11389                                message: format!(
11390                                    "axonendpoint '{}' declares `query: {{ … }}` \
11391                                     more than once. The query-parameter block \
11392                                     is unique per endpoint; combine all params \
11393                                     into a single block.",
11394                                    node.name,
11395                                ),
11396                                line: lbrace_tok.line,
11397                                column: lbrace_tok.column,
11398                                ..Default::default()
11399                            });
11400                        }
11401                        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11402                            let name_tok = self.consume(TokenType::Identifier)?;
11403                            let field_name = name_tok.value.clone();
11404                            // Duplicate detection within the block.
11405                            if node
11406                                .query_params
11407                                .iter()
11408                                .any(|f| f.name == field_name)
11409                            {
11410                                return Err(ParseError {
11411                                    message: format!(
11412                                        "axonendpoint '{}' declares duplicate \
11413                                         query param '{}' inside `query: {{ … }}`. \
11414                                         Each name must appear at most once \
11415                                         .",
11416                                        node.name, field_name,
11417                                    ),
11418                                    line: name_tok.line,
11419                                    column: name_tok.column,
11420                                    ..Default::default()
11421                                });
11422                            }
11423                            self.consume(TokenType::Colon)?;
11424                            let type_expr = self.parse_type_expr()?;
11425                            // v1.32.0 (D2 robustness) — reject generic
11426                            // type expressions on query params. The
11427                            // closed catalog is 5 primitives; container
11428                            // types (`Optional<T>`, `List<T>`, etc.)
11429                            // would mislead the adopter into thinking
11430                            // they bind multi-value query strings
11431                            // (deferred per plan vivo section 7) or that
11432                            // `Optional<Text>` is the canonical way to
11433                            // declare an optional query (it's NOT —
11434                            // `Text?` is). Surface the canonical syntax
11435                            // verbatim so the fix is obvious.
11436                            if !type_expr.generic_param.is_empty() {
11437                                let canonical_hint = if type_expr.name == "Optional" {
11438                                    format!(
11439                                        " Use `{}?` (the `?` suffix) for an \
11440                                         optional query param instead of \
11441                                         `Optional<{}>`.",
11442                                        type_expr.generic_param,
11443                                        type_expr.generic_param,
11444                                    )
11445                                } else if type_expr.name == "List" {
11446                                    " Multi-value query params (e.g. `?tag=a&tag=b`) \
11447                                     are honest-deferred from v1.38.5; bind a \
11448                                     single-value `Text` query param and parse \
11449                                     the value inside the flow."
11450                                        .to_string()
11451                                } else {
11452                                    String::new()
11453                                };
11454                                return Err(ParseError {
11455                                    message: format!(
11456                                        "axonendpoint '{}' query param '{}' uses \
11457                                         a generic type `{}<{}>`. Query params \
11458                                         take a primitive type from the closed \
11459                                         catalog ({}); the `?` suffix marks \
11460                                         optional.{} .",
11461                                        node.name,
11462                                        field_name,
11463                                        type_expr.name,
11464                                        type_expr.generic_param,
11465                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | "),
11466                                        canonical_hint,
11467                                    ),
11468                                    line: type_expr.loc.line,
11469                                    column: type_expr.loc.column,
11470                                    ..Default::default()
11471                                });
11472                            }
11473                            // Validate against the closed catalog. A
11474                            // miss surfaces a v1.20.0-style smart-suggest
11475                            // hint when within edit-distance 2.
11476                            if !axonendpoint_is_valid_query_param_type(&type_expr.name) {
11477                                // `smart_suggest::suggest_for` returns
11478                                // pre-formatted prose like
11479                                // "Did you mean `Text`?" or
11480                                // "Did you mean `Text` or `Int`?" (empty
11481                                // when no candidate within edit-distance
11482                                // 2). Concatenate without re-wrapping.
11483                                let hint = crate::smart_suggest::suggest_for(
11484                                    &type_expr.name,
11485                                    AXONENDPOINT_QUERY_PARAM_TYPES,
11486                                );
11487                                let hint_text = if hint.is_empty() {
11488                                    format!(
11489                                        " Expected one of: {}.",
11490                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | ")
11491                                    )
11492                                } else {
11493                                    format!(
11494                                        " {} Expected one of: {}.",
11495                                        hint,
11496                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | ")
11497                                    )
11498                                };
11499                                return Err(ParseError {
11500                                    message: format!(
11501                                        "axonendpoint '{}' query param '{}' has \
11502                                         unsupported type '{}'.{} .",
11503                                        node.name, field_name, type_expr.name,
11504                                        hint_text,
11505                                    ),
11506                                    line: type_expr.loc.line,
11507                                    column: type_expr.loc.column,
11508                                    ..Default::default()
11509                                });
11510                            }
11511                            node.query_params.push(TypeField {
11512                                name: field_name,
11513                                type_expr,
11514                                loc: Loc {
11515                                    line: name_tok.line,
11516                                    column: name_tok.column,
11517                                },
11518                            });
11519                            // Trailing comma is optional; the next loop
11520                            // iteration handles `}` cleanly. Accept both
11521                            // `name: Type, name: Type` AND `name: Type
11522                            // name: Type` (the existing parser style is
11523                            // forgiving about list separators).
11524                            if self.check(TokenType::Comma) {
11525                                self.advance();
11526                            }
11527                            let _ = block_line; // suppress unused warning
11528                        }
11529                        self.consume(TokenType::RBrace)?;
11530                    },
11531                    "execute" => node.execute_flow = self.consume_any_ident_or_kw()?.value.clone(),
11532                    "output" => {
11533                        // v1.31.0 — promote axonendpoint `output:`
11534                        // parsing from a single token to the full
11535                        // generic-aware type expression (mirroring
11536                        // `parse_step` for FlowStep::Step which already
11537                        // uses `parse_output_type_string`).
11538                        //
11539                        // Pre-38.x.f: `output: List<Item>` captured only
11540                        // `"List"`, dropping `<Item>` (next tokens were
11541                        // either left unconsumed or absorbed by the
11542                        // following field). v1.39.0's narrow cardinality
11543                        // gate happened to fire correctly for `output: T`
11544                        // + retrieve-tail because the singular-detection
11545                        // path used `!starts_with("List<")` — but the
11546                        // SYMMETRIC `output: List<T>` + singular-tail
11547                        // case (38.x.f D3) needs the FULL `List<T>`
11548                        // shape captured; without it the gate sees
11549                        // `"List"` and misclassifies as Singular.
11550                        node.output_type = self.parse_output_type_string()?;
11551                    }
11552                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
11553                    // v2.38.0 — the `cors: <Name>` reference.
11554                    "cors" => node.cors_ref = self.consume_any_ident_or_kw()?.value.clone(),
11555                    "retries" => node.retries = self.parse_optional_int(),
11556                    "timeout" => {
11557                        let t = self.current().clone();
11558                        self.advance();
11559                        node.timeout = t.value.clone();
11560                    }
11561                    "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
11562                    "replay" => {
11563                        // v1.23.0 (D9 plan-vivo) — Replay-token binding.
11564                        // Boolean `replay: true | false`. Default (when
11565                        // omitted) is method-derived at deploy-time:
11566                        // POST/PUT → true, GET/DELETE → false. Explicit
11567                        // declaration sets `replay_explicit = true` so
11568                        // the runtime knows NOT to override.
11569                        let value_tok = self.consume(TokenType::Bool)?;
11570                        node.replay = value_tok.value.eq_ignore_ascii_case("true");
11571                        node.replay_explicit = true;
11572                    }
11573                    // v2.44.0 — `public: true | false`, the explicit
11574                    // authorization-coverage opt-out (doctrine
11575                    // `every_boundary_is_guarded`). Mirrors `replay:`'s bool
11576                    // parse. Default false; the v2.44.0 rule (`axon-T890`)
11577                    // requires a covering discipline OR `public: true`.
11578                    "public" => {
11579                        let value_tok = self.consume(TokenType::Bool)?;
11580                        node.public = value_tok.value.eq_ignore_ascii_case("true");
11581                    }
11582                    "requires" => {
11583                        // v1.23.0 (D8) — Auth scope per axonendpoint.
11584                        // Closed slug grammar
11585                        // `^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$` enforced
11586                        // at parse time with smart-suggest-style hint.
11587                        // Empty list means "no auth gate" (D9 backwards-
11588                        // compat). Cross-stack with Python parser.
11589                        let bracket_tok = self.current().clone();
11590                        let items = self.parse_bracketed_dot_identifiers()?;
11591                        for slug in &items {
11592                            if !is_valid_capability_slug(slug) {
11593                                return Err(ParseError {
11594                                    message: format!(
11595                                        "Invalid capability slug '{slug}' in axonendpoint '{}' \
11596                                         `requires:`. Capability slugs must match \
11597                                         ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
11598                                         lowercase identifiers starting with a letter. Examples: \
11599                                         `admin`, `legal.read`, `hipaa.phi.read`.",
11600                                        node.name
11601                                    ),
11602                                    line: bracket_tok.line,
11603                                    column: bracket_tok.column,
11604                                    ..Default::default()
11605                                });
11606                            }
11607                        }
11608                        node.requires_capabilities = items;
11609                    }
11610                    // v1.21.0 — HTTP transport enum (D2 closed) + keepalive (D6 closed).
11611                    // Mirrors `axon/compiler/parser.py` `_parse_axonendpoint`.
11612                    // Drift-gate corpus verifies byte-identical parse cross-stack.
11613                    "transport" => {
11614                        let value_tok = self.consume_any_ident_or_kw()?;
11615                        let value = &value_tok.value;
11616                        if !axonendpoint_is_valid_transport(value) {
11617                            let hint = crate::smart_suggest::suggest_for(
11618                                value,
11619                                AXONENDPOINT_TRANSPORT_VALUES,
11620                            );
11621                            let base = format!(
11622                                "Invalid transport '{}' in axonendpoint '{}'.",
11623                                value, node.name
11624                            );
11625                            let message = if hint.is_empty() {
11626                                format!("{base} expected json | sse | ndjson, found {value}")
11627                            } else {
11628                                format!(
11629                                    "{base} {hint} (expected json | sse | ndjson, found {value})"
11630                                )
11631                            };
11632                            return Err(ParseError {
11633                                message,
11634                                line: value_tok.line,
11635                                column: value_tok.column,
11636                                ..Default::default()
11637                            });
11638                        }
11639                        node.transport = value.clone();
11640                        // v1.22.0 D1 — mark the field as explicitly
11641                        // declared so the type-checker's implicit-transport
11642                        // inference knows NOT to override this value with
11643                        // the produces_stream-driven inference.
11644                        node.transport_explicit = true;
11645                        // v1.28.0 — Optional dialect
11646                        // parametrization: `transport: sse(<dialect>)`.
11647                        // Only valid when the base value is `sse`
11648                        // (json + ndjson dialects are the dialects
11649                        // themselves; `json(<x>)` / `ndjson(<x>)`
11650                        // would be parse errors caught below).
11651                        if self.check(TokenType::LParen) {
11652                            if value != "sse" {
11653                                let tok = self.current().clone();
11654                                return Err(ParseError {
11655                                    message: format!(
11656                                        "Dialect parametrization \
11657                                         `transport: {value}(<dialect>)` is \
11658                                         only valid for `sse`; got \
11659                                         `{value}` in axonendpoint '{}'.",
11660                                        node.name
11661                                    ),
11662                                    line: tok.line,
11663                                    column: tok.column,
11664                                    ..Default::default()
11665                                });
11666                            }
11667                            self.advance(); // consume LParen
11668                            let dialect_tok = self.consume_any_ident_or_kw()?;
11669                            let dialect = dialect_tok.value.clone();
11670                            if !AXONENDPOINT_TRANSPORT_DIALECTS
11671                                .iter()
11672                                .any(|&d| d == dialect)
11673                            {
11674                                let hint = crate::smart_suggest::suggest_for(
11675                                    &dialect,
11676                                    AXONENDPOINT_TRANSPORT_DIALECTS,
11677                                );
11678                                let base = format!(
11679                                    "Invalid SSE dialect '{dialect}' in axonendpoint '{}'.",
11680                                    node.name
11681                                );
11682                                let message = if hint.is_empty() {
11683                                    format!(
11684                                        "{base} expected axon | openai | kimi | glm | anthropic, found {dialect}"
11685                                    )
11686                                } else {
11687                                    format!(
11688                                        "{base} {hint} (expected axon | openai | kimi | glm | anthropic, found {dialect})"
11689                                    )
11690                                };
11691                                return Err(ParseError {
11692                                    message,
11693                                    line: dialect_tok.line,
11694                                    column: dialect_tok.column,
11695                                    ..Default::default()
11696                                });
11697                            }
11698                            // Closing RParen.
11699                            let rparen_tok = self.current().clone();
11700                            if !self.check(TokenType::RParen) {
11701                                return Err(ParseError {
11702                                    message: format!(
11703                                        "Expected `)` after dialect name \
11704                                         in axonendpoint '{}' \
11705                                         (transport: sse(<dialect>) grammar).",
11706                                        node.name
11707                                    ),
11708                                    line: rparen_tok.line,
11709                                    column: rparen_tok.column,
11710                                    ..Default::default()
11711                                });
11712                            }
11713                            self.advance(); // consume RParen
11714                            node.transport_dialect = dialect;
11715                        }
11716                    }
11717                    "keepalive" => {
11718                        // Accepts either a DURATION token (e.g. `15s`) or
11719                        // an ident-like token. Validation against the
11720                        // closed enum {5s, 15s, 30s, 60s} happens after.
11721                        let value_tok = self.current().clone();
11722                        self.advance();
11723                        let value = &value_tok.value;
11724                        if !axonendpoint_is_valid_keepalive(value) {
11725                            let hint = crate::smart_suggest::suggest_for(
11726                                value,
11727                                AXONENDPOINT_KEEPALIVE_VALUES,
11728                            );
11729                            let base = format!(
11730                                "Invalid keepalive '{}' in axonendpoint '{}'.",
11731                                value, node.name
11732                            );
11733                            let message = if hint.is_empty() {
11734                                format!("{base} expected 5s | 15s | 30s | 60s, found {value}")
11735                            } else {
11736                                format!(
11737                                    "{base} {hint} (expected 5s | 15s | 30s | 60s, found {value})"
11738                                )
11739                            };
11740                            return Err(ParseError {
11741                                message,
11742                                line: value_tok.line,
11743                                column: value_tok.column,
11744                                ..Default::default()
11745                            });
11746                        }
11747                        node.keepalive = value.clone();
11748                    }
11749                    "backend" => {
11750                        // v1.31.0 (D2) — declared execution backend.
11751                        // Closed catalog `CANONICAL_PROVIDERS ∪ {auto,
11752                        // stub}`; an unknown name is a parse error with
11753                        // a smart-suggest hint (the same discipline as
11754                        // `method`/`transport`/`keepalive`). The
11755                        // type-checker re-validates defensively for
11756                        // ASTs built outside the parser (LSP, tests).
11757                        let value_tok = self.consume_any_ident_or_kw()?;
11758                        let value = &value_tok.value;
11759                        if !axonendpoint_is_valid_backend(value) {
11760                            let hint = crate::smart_suggest::suggest_for(
11761                                value,
11762                                AXONENDPOINT_BACKEND_VALUES,
11763                            );
11764                            let expected = AXONENDPOINT_BACKEND_VALUES.join(" | ");
11765                            let base = format!(
11766                                "Invalid backend '{}' in axonendpoint '{}'.",
11767                                value, node.name
11768                            );
11769                            let message = if hint.is_empty() {
11770                                format!("{base} expected {expected}, found {value}")
11771                            } else {
11772                                format!(
11773                                    "{base} {hint} (expected {expected}, found {value})"
11774                                )
11775                            };
11776                            return Err(ParseError {
11777                                message,
11778                                line: value_tok.line,
11779                                column: value_tok.column,
11780                                ..Default::default()
11781                            });
11782                        }
11783                        node.backend = value.clone();
11784                    }
11785                    _ => self.skip_value(),
11786                }
11787            } else if self.check(TokenType::LBrace) {
11788                self.skip_braced_block()?;
11789            }
11790        }
11791        self.consume(TokenType::RBrace)?;
11792        Ok(node)
11793    }
11794
11795    // ── Numeric helpers for Tier 2 field parsing ────────────────────
11796
11797    fn parse_optional_int(&mut self) -> Option<i64> {
11798        let tok = self.current().clone();
11799        match tok.ttype {
11800            TokenType::Integer => {
11801                self.advance();
11802                tok.value.parse::<i64>().ok()
11803            }
11804            _ => {
11805                self.advance();
11806                None
11807            }
11808        }
11809    }
11810
11811    fn parse_optional_float(&mut self) -> Option<f64> {
11812        let tok = self.current().clone();
11813        match tok.ttype {
11814            TokenType::Float | TokenType::Integer => {
11815                self.advance();
11816                tok.value.parse::<f64>().ok()
11817            }
11818            _ => {
11819                self.advance();
11820                None
11821            }
11822        }
11823    }
11824
11825    // ── LAMBDA DATA (ΛD) ──────────────────────────────────────────
11826
11827    fn parse_lambda_data(&mut self) -> Result<LambdaDataDefinition, ParseError> {
11828        let tok = self.consume(TokenType::Lambda)?;
11829        let name = self.consume(TokenType::Identifier)?;
11830        self.consume(TokenType::LBrace)?;
11831
11832        let mut node = LambdaDataDefinition {
11833            name: name.value.clone(),
11834            ontology: String::new(),
11835            certainty: 1.0,
11836            temporal_frame_start: String::new(),
11837            temporal_frame_end: String::new(),
11838            provenance: String::new(),
11839            derivation: String::new(),
11840            loc: Loc {
11841                line: tok.line,
11842                column: tok.column,
11843            },
11844            leading_trivia: Vec::new(),
11845            trailing_trivia: Vec::new(),
11846        };
11847
11848        while !self.check(TokenType::RBrace) {
11849            let field = self.current().clone();
11850            match field.ttype {
11851                TokenType::Ontology => {
11852                    self.advance();
11853                    self.consume(TokenType::Colon)?;
11854                    node.ontology = self.consume(TokenType::StringLit)?.value.clone();
11855                }
11856                TokenType::Certainty => {
11857                    self.advance();
11858                    self.consume(TokenType::Colon)?;
11859                    let val = self.current().clone();
11860                    match val.ttype {
11861                        TokenType::Float => {
11862                            self.advance();
11863                            node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
11864                        }
11865                        TokenType::Integer => {
11866                            self.advance();
11867                            node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
11868                        }
11869                        _ => {
11870                            return Err(ParseError {
11871                                message: format!(
11872                                    "Expected number for certainty, got '{}'",
11873                                    val.value
11874                                ),
11875                                line: val.line,
11876                                column: val.column,
11877                                                            ..Default::default()
11878                            });
11879                        }
11880                    }
11881                }
11882                TokenType::TemporalFrame => {
11883                    self.advance();
11884                    self.consume(TokenType::Colon)?;
11885                    node.temporal_frame_start = self.consume(TokenType::StringLit)?.value.clone();
11886                    // Optional second string for end frame
11887                    if self.check(TokenType::StringLit) {
11888                        node.temporal_frame_end = self.consume(TokenType::StringLit)?.value.clone();
11889                    }
11890                }
11891                TokenType::Provenance => {
11892                    self.advance();
11893                    self.consume(TokenType::Colon)?;
11894                    node.provenance = self.consume(TokenType::StringLit)?.value.clone();
11895                }
11896                TokenType::Derivation => {
11897                    self.advance();
11898                    self.consume(TokenType::Colon)?;
11899                    let d = self.current().clone();
11900                    self.advance();
11901                    node.derivation = d.value.clone();
11902                }
11903                _ => {
11904                    // Skip unknown fields gracefully
11905                    self.advance();
11906                    if self.check(TokenType::Colon) {
11907                        self.advance();
11908                        self.skip_value();
11909                    }
11910                }
11911            }
11912        }
11913
11914        self.consume(TokenType::RBrace)?;
11915        Ok(node)
11916    }
11917
11918    fn parse_lambda_data_apply(&mut self) -> Result<LambdaDataApplyNode, ParseError> {
11919        let tok = self.consume(TokenType::Lambda)?;
11920        let lambda_name = self.consume(TokenType::Identifier)?;
11921
11922        // Expect "on" keyword (parsed as identifier since it's not reserved)
11923        let on_tok = self.current().clone();
11924        self.advance();
11925        if on_tok.value != "on" {
11926            return Err(ParseError {
11927                message: format!(
11928                    "Expected 'on' after lambda data name in flow step, got '{}'",
11929                    on_tok.value
11930                ),
11931                line: on_tok.line,
11932                column: on_tok.column,
11933                            ..Default::default()
11934            });
11935        }
11936
11937        let target = self.current().clone();
11938        self.advance();
11939
11940        let mut output_type = String::new();
11941        if self.check(TokenType::Arrow) {
11942            self.advance();
11943            output_type = self.consume(TokenType::Identifier)?.value.clone();
11944        }
11945
11946        Ok(LambdaDataApplyNode {
11947            lambda_data_name: lambda_name.value.clone(),
11948            target: target.value.clone(),
11949            output_type,
11950            loc: Loc {
11951                line: tok.line,
11952                column: tok.column,
11953            },
11954        })
11955    }
11956
11957    // ── GENERIC (Tier 2+) ────────────────────────────────────────
11958
11959    fn parse_generic_declaration(&mut self) -> Result<Declaration, ParseError> {
11960        let kw_tok = self.current().clone();
11961        self.advance(); // consume keyword
11962
11963        // Try to consume a name (identifier or keyword-as-name)
11964        let name = if self.current().ttype == TokenType::Identifier {
11965            let n = self.current().value.clone();
11966            self.advance();
11967            n
11968        } else if !self.check(TokenType::LBrace)
11969            && !self.check(TokenType::LParen)
11970            && !self.check(TokenType::Eof)
11971            && self
11972                .current()
11973                .value
11974                .chars()
11975                .all(|c| c.is_alphanumeric() || c == '_')
11976        {
11977            let n = self.current().value.clone();
11978            self.advance();
11979            n
11980        } else {
11981            String::new()
11982        };
11983
11984        // Skip optional parens: (...)
11985        if self.check(TokenType::LParen) {
11986            self.advance();
11987            let mut depth = 1u32;
11988            while depth > 0 && !self.check(TokenType::Eof) {
11989                if self.check(TokenType::LParen) {
11990                    depth += 1;
11991                } else if self.check(TokenType::RParen) {
11992                    depth -= 1;
11993                }
11994                self.advance();
11995            }
11996        }
11997
11998        // Skip tokens until LBrace or next declaration
11999        while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
12000            if self.check(TokenType::Eof) {
12001                break;
12002            }
12003            self.advance();
12004        }
12005
12006        // Skip braced block if present
12007        if self.check(TokenType::LBrace) {
12008            self.skip_braced_block()?;
12009        }
12010
12011        Ok(Declaration::Generic(GenericDeclaration {
12012            keyword: kw_tok.value,
12013            name,
12014            loc: Loc {
12015                line: kw_tok.line,
12016                column: kw_tok.column,
12017            },
12018            leading_trivia: Vec::new(),
12019            trailing_trivia: Vec::new(),
12020        }))
12021    }
12022
12023    // ──────────────────────────────────────────────────────────────────
12024    // v1.6.0 — Mobile Typed Channels parsers
12025    // (paper_mobile_channels.md section 3 + plan/the design plan)
12026    //  Direct port of axon/compiler/parser.py:_parse_channel/emit/publish/discover.
12027    // ──────────────────────────────────────────────────────────────────
12028
12029    /// Parse: `channel Name { message, qos, lifetime, persistence, shield }`.
12030    fn parse_channel(&mut self) -> Result<ChannelDefinition, ParseError> {
12031        let tok = self.consume(TokenType::Channel)?;
12032        let name = self.consume(TokenType::Identifier)?.value;
12033        let mut node = ChannelDefinition {
12034            name: name.clone(),
12035            message: String::new(),
12036            qos: "at_least_once".to_string(),
12037            lifetime: "affine".to_string(),
12038            persistence: "ephemeral".to_string(),
12039            shield_ref: String::new(),
12040            loc: Loc {
12041                line: tok.line,
12042                column: tok.column,
12043            },
12044            leading_trivia: Vec::new(),
12045            trailing_trivia: Vec::new(),
12046        };
12047        self.consume(TokenType::LBrace)?;
12048        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
12049            let field_tok = self.current().clone();
12050            let field_name = field_tok.value.clone();
12051            self.advance();
12052            if !self.check(TokenType::Colon) {
12053                if self.check(TokenType::LBrace) {
12054                    self.skip_braced_block()?;
12055                }
12056                continue;
12057            }
12058            self.advance();
12059            match field_name.as_str() {
12060                "message" => node.message = self.parse_channel_message_type()?,
12061                "qos" => {
12062                    let q_tok = self.consume_any_ident_or_kw()?;
12063                    if !matches!(
12064                        q_tok.value.as_str(),
12065                        "at_most_once" | "at_least_once" | "exactly_once" | "broadcast" | "queue"
12066                    ) {
12067                        return Err(ParseError {
12068                            message: format!(
12069                                "Invalid qos '{}' in channel '{}' — \
12070                                 expected at_most_once | at_least_once | \
12071                                 exactly_once | broadcast | queue",
12072                                q_tok.value, name
12073                            ),
12074                            line: q_tok.line,
12075                            column: q_tok.column,
12076                                                    ..Default::default()
12077                        });
12078                    }
12079                    node.qos = q_tok.value;
12080                }
12081                "lifetime" => {
12082                    let lt_tok = self.consume_any_ident_or_kw()?;
12083                    if !matches!(lt_tok.value.as_str(), "linear" | "affine" | "persistent") {
12084                        return Err(ParseError {
12085                            message: format!(
12086                                "Invalid lifetime '{}' in channel '{}' — \
12087                                 expected linear | affine | persistent",
12088                                lt_tok.value, name
12089                            ),
12090                            line: lt_tok.line,
12091                            column: lt_tok.column,
12092                                                    ..Default::default()
12093                        });
12094                    }
12095                    node.lifetime = lt_tok.value;
12096                }
12097                "persistence" => {
12098                    let p_tok = self.consume_any_ident_or_kw()?;
12099                    if !matches!(p_tok.value.as_str(), "ephemeral" | "persistent_axonstore") {
12100                        return Err(ParseError {
12101                            message: format!(
12102                                "Invalid persistence '{}' in channel '{}' — \
12103                                 expected ephemeral | persistent_axonstore",
12104                                p_tok.value, name
12105                            ),
12106                            line: p_tok.line,
12107                            column: p_tok.column,
12108                                                    ..Default::default()
12109                        });
12110                    }
12111                    node.persistence = p_tok.value;
12112                }
12113                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
12114                _ => self.skip_value(),
12115            }
12116        }
12117        self.consume(TokenType::RBrace)?;
12118        Ok(node)
12119    }
12120
12121    /// Parse a `message:` value, supporting nested `Channel<…>`
12122    /// (second-order session types — paper section 3.3).
12123    fn parse_channel_message_type(&mut self) -> Result<String, ParseError> {
12124        let head = self.consume(TokenType::Identifier)?;
12125        let mut spelling = head.value;
12126        if self.check(TokenType::Lt) {
12127            self.advance();
12128            let inner = self.parse_channel_message_type()?;
12129            self.consume(TokenType::Gt)?;
12130            spelling = format!("{}<{}>", spelling, inner);
12131        }
12132        Ok(spelling)
12133    }
12134
12135    /// Parse: `emit ChannelName(value_ref)` — Chan-Output / Chan-Mobility.
12136    ///
12137    /// `value_ref` accepts a bare identifier (variable / channel name for
12138    /// mobility) or a dotted path (`Step.output.field`) referencing a prior
12139    /// step result (v1.6.0 — runtime resolves via ContextManager).
12140    fn parse_emit_step(&mut self) -> Result<FlowStep, ParseError> {
12141        let tok = self.consume(TokenType::Emit)?;
12142        let channel = self.consume(TokenType::Identifier)?.value;
12143        self.consume(TokenType::LParen)?;
12144        let value = self.parse_emit_value_ref()?;
12145        self.consume(TokenType::RParen)?;
12146        Ok(FlowStep::Emit(EmitStatement {
12147            channel_ref: channel,
12148            value_ref: value,
12149            loc: Loc {
12150                line: tok.line,
12151                column: tok.column,
12152            },
12153        }))
12154    }
12155
12156    /// v2.46.0 — parse `mint <Credential> as <binding>`. The credential
12157    /// reference must resolve to a declared `credential` (`axon-T895`,
12158    /// type-checker); the binding is a fresh flow-scoped name receiving the
12159    /// raw bearer string. Both tokens are required — a `mint` with no
12160    /// binding would mint authority into the void.
12161    fn parse_mint_step(&mut self) -> Result<FlowStep, ParseError> {
12162        let tok = self.consume(TokenType::Mint)?;
12163        let credential_ref = self.consume(TokenType::Identifier)?.value;
12164        self.consume(TokenType::As)?;
12165        let binding = self.consume(TokenType::Identifier)?.value;
12166        Ok(FlowStep::Mint(MintStep {
12167            credential_ref,
12168            binding,
12169            loc: Loc {
12170                line: tok.line,
12171                column: tok.column,
12172            },
12173        }))
12174    }
12175
12176    /// v2.48.0 — parse `rotate <SecretsStore> [where "<filter>"] with
12177    /// <Tool> as <binding>` (doctrine `rotation_without_revelation`).
12178    ///
12179    /// All three anchors are grammar, not convention: the store names WHAT
12180    /// may rotate (a `backend: secrets` class view — `axon-T898` in the
12181    /// type-checker), the tool names WHO performs the exchange
12182    /// (`axon-T899`), and the binding receives the metadata-only summary —
12183    /// a `rotate` without a binding would renew authority with no
12184    /// observable outcome, so `as` is REQUIRED (the `mint` posture). The
12185    /// `where` filter is optional (v2.21.0 string grammar, proven against the
12186    /// synthesized metadata schema); omitting it rotates the WHOLE class —
12187    /// the deliberate post-breach bulk shape. `with` is a soft keyword
12188    /// (not a lexer token): reserving it globally would break every
12189    /// adopter identifier named `with`.
12190    fn parse_rotate_step(&mut self) -> Result<FlowStep, ParseError> {
12191        let tok = self.consume(TokenType::Rotate)?;
12192        let store_ref = self.consume(TokenType::Identifier)?.value;
12193        let mut where_expr = String::new();
12194        if self.check(TokenType::Where) {
12195            self.advance();
12196            where_expr = self.consume(TokenType::StringLit)?.value.clone();
12197        }
12198        let with_tok = self.current().clone();
12199        if with_tok.value != "with" {
12200            return Err(ParseError {
12201                message: format!(
12202                    "Expected `with <Tool>` after `rotate {store_ref}{}`, found '{}'. \
12203                     A rotation names the tool that performs the renewal exchange: \
12204                     `rotate {store_ref} [where \"<filter>\"] with <Tool> as <binding>`.",
12205                    if where_expr.is_empty() { "" } else { " where …" },
12206                    with_tok.value
12207                ),
12208                line: with_tok.line,
12209                column: with_tok.column,
12210                ..Default::default()
12211            });
12212        }
12213        self.advance();
12214        let tool_ref = self.consume(TokenType::Identifier)?.value;
12215        self.consume(TokenType::As)?;
12216        let binding = self.consume(TokenType::Identifier)?.value;
12217        Ok(FlowStep::Rotate(RotateStep {
12218            store_ref,
12219            where_expr,
12220            tool_ref,
12221            binding,
12222            loc: Loc {
12223                line: tok.line,
12224                column: tok.column,
12225            },
12226        }))
12227    }
12228
12229    /// Parse: `IDENTIFIER ('.' (IDENTIFIER | keyword))*` → dot-joined string
12230    /// (v1.6.0).
12231    ///
12232    /// Mirrors the Python `_parse_emit_value_ref` helper exactly so the IR
12233    /// JSON for `emit Hello(Build.output)` is byte-identical between the
12234    /// two reference implementations.
12235    ///
12236    /// The HEAD must be a real ``Identifier``. Subsequent segments after a
12237    /// `.` may be identifiers OR keywords — common field names like
12238    /// ``output``, ``result``, ``message``, ``state``, etc. are reserved
12239    /// words in Axon but adopters must be able to write them as
12240    /// dotted-access segments. The accepting predicate:
12241    ///   - the lexer carried a non-empty `value` (every Word-like token does)
12242    ///   - the value's first byte is a letter or underscore (filters out
12243    ///     punctuation tokens such as ',', '{', etc.)
12244    fn parse_emit_value_ref(&mut self) -> Result<String, ParseError> {
12245        let head = self.consume(TokenType::Identifier)?.value;
12246        let mut parts = vec![head];
12247        while self.check(TokenType::Dot) {
12248            self.advance(); // consume '.'
12249            let next_tok = self.current().clone();
12250            let valid = !next_tok.value.is_empty()
12251                && next_tok.value.as_bytes()[0].is_ascii_alphabetic()
12252                || next_tok.value.starts_with('_');
12253            if !valid {
12254                return Err(ParseError {
12255                    message: format!(
12256                        "Expected identifier or keyword after '.' in dotted \
12257                         access, found {:?}",
12258                        next_tok.value
12259                    ),
12260                    line: next_tok.line,
12261                    column: next_tok.column,
12262                                    ..Default::default()
12263                });
12264            }
12265            self.advance();
12266            parts.push(next_tok.value);
12267        }
12268        Ok(parts.join("."))
12269    }
12270
12271    /// Parse: `publish ChannelName within ShieldName` — Publish-Ext (D8).
12272    fn parse_publish_step(&mut self) -> Result<FlowStep, ParseError> {
12273        let tok = self.consume(TokenType::Publish)?;
12274        let channel = self.consume(TokenType::Identifier)?.value;
12275        self.consume(TokenType::Within)?;
12276        let shield = self.consume(TokenType::Identifier)?.value;
12277        Ok(FlowStep::Publish(PublishStatement {
12278            channel_ref: channel,
12279            shield_ref: shield,
12280            loc: Loc {
12281                line: tok.line,
12282                column: tok.column,
12283            },
12284        }))
12285    }
12286
12287    /// Parse: `discover ChannelName as alias` — dual of publish.
12288    fn parse_discover_step(&mut self) -> Result<FlowStep, ParseError> {
12289        let tok = self.consume(TokenType::Discover)?;
12290        let cap = self.consume(TokenType::Identifier)?.value;
12291        self.consume(TokenType::As)?;
12292        let alias = self.consume(TokenType::Identifier)?.value;
12293        Ok(FlowStep::Discover(DiscoverStatement {
12294            capability_ref: cap,
12295            alias,
12296            loc: Loc {
12297                line: tok.line,
12298                column: tok.column,
12299            },
12300        }))
12301    }
12302}
12303
12304// ── v1.6.0 — Mobile Typed Channels parser tests ─────────────────────
12305
12306#[cfg(test)]
12307mod parser_tests {
12308    use super::*;
12309    use crate::lexer::Lexer;
12310
12311    fn parse(src: &str) -> Result<Program, ParseError> {
12312        let tokens = Lexer::new(src, "<test>").tokenize().expect("lex");
12313        Parser::new(tokens).parse()
12314    }
12315
12316    #[test]
12317    fn channel_full_parses() {
12318        let src = r#"channel C { message: Order qos: at_least_once lifetime: affine persistence: ephemeral shield: Gate }"#;
12319        let prog = parse(src).expect("parse");
12320        match &prog.declarations[0] {
12321            Declaration::Channel(c) => {
12322                assert_eq!(c.name, "C");
12323                assert_eq!(c.message, "Order");
12324                assert_eq!(c.qos, "at_least_once");
12325                assert_eq!(c.lifetime, "affine");
12326                assert_eq!(c.persistence, "ephemeral");
12327                assert_eq!(c.shield_ref, "Gate");
12328            }
12329            _ => panic!("expected ChannelDefinition"),
12330        }
12331    }
12332
12333    #[test]
12334    fn channel_defaults_match_paper_d1() {
12335        let prog = parse("channel C { message: Order }").expect("parse");
12336        if let Declaration::Channel(c) = &prog.declarations[0] {
12337            assert_eq!(c.qos, "at_least_once"); // default
12338            assert_eq!(c.lifetime, "affine"); // D1 default
12339            assert_eq!(c.persistence, "ephemeral");
12340            assert_eq!(c.shield_ref, "");
12341        } else {
12342            panic!("expected ChannelDefinition");
12343        }
12344    }
12345
12346    #[test]
12347    fn channel_second_order_message_type_parses() {
12348        let prog = parse("channel C { message: Channel<Order> }").expect("parse");
12349        if let Declaration::Channel(c) = &prog.declarations[0] {
12350            assert_eq!(c.message, "Channel<Order>");
12351        } else {
12352            panic!("expected ChannelDefinition");
12353        }
12354    }
12355
12356    #[test]
12357    fn channel_nested_channel_message_type_parses() {
12358        let prog = parse("channel C { message: Channel<Channel<Order>> }").expect("parse");
12359        if let Declaration::Channel(c) = &prog.declarations[0] {
12360            assert_eq!(c.message, "Channel<Channel<Order>>");
12361        } else {
12362            panic!("expected ChannelDefinition");
12363        }
12364    }
12365
12366    #[test]
12367    fn channel_invalid_qos_rejected() {
12368        let err = parse("channel C { message: T qos: bogus }").unwrap_err();
12369        assert!(err.message.contains("Invalid qos"), "got {}", err.message);
12370    }
12371
12372    #[test]
12373    fn channel_invalid_lifetime_rejected() {
12374        let err = parse("channel C { message: T lifetime: eternal }").unwrap_err();
12375        assert!(
12376            err.message.contains("Invalid lifetime"),
12377            "got {}",
12378            err.message
12379        );
12380    }
12381
12382    #[test]
12383    fn channel_invalid_persistence_rejected() {
12384        let err = parse("channel C { message: T persistence: forever }").unwrap_err();
12385        assert!(
12386            err.message.contains("Invalid persistence"),
12387            "got {}",
12388            err.message
12389        );
12390    }
12391
12392    #[test]
12393    fn emit_value_parses() {
12394        let src = "flow f() -> Out { emit C(payload) }";
12395        let prog = parse(src).expect("parse");
12396        if let Declaration::Flow(f) = &prog.declarations[0] {
12397            match &f.body[0] {
12398                FlowStep::Emit(e) => {
12399                    assert_eq!(e.channel_ref, "C");
12400                    assert_eq!(e.value_ref, "payload");
12401                }
12402                other => panic!("expected Emit, got {:?}", other),
12403            }
12404        } else {
12405            panic!("expected Flow");
12406        }
12407    }
12408
12409    #[test]
12410    fn publish_within_shield_parses() {
12411        let src = "flow f() -> Cap { publish C within Gate }";
12412        let prog = parse(src).expect("parse");
12413        if let Declaration::Flow(f) = &prog.declarations[0] {
12414            match &f.body[0] {
12415                FlowStep::Publish(p) => {
12416                    assert_eq!(p.channel_ref, "C");
12417                    assert_eq!(p.shield_ref, "Gate");
12418                }
12419                other => panic!("expected Publish, got {:?}", other),
12420            }
12421        } else {
12422            panic!("expected Flow");
12423        }
12424    }
12425
12426    #[test]
12427    fn discover_with_alias_parses() {
12428        let src = "flow f() -> Out { discover C as ch }";
12429        let prog = parse(src).expect("parse");
12430        if let Declaration::Flow(f) = &prog.declarations[0] {
12431            match &f.body[0] {
12432                FlowStep::Discover(d) => {
12433                    assert_eq!(d.capability_ref, "C");
12434                    assert_eq!(d.alias, "ch");
12435                }
12436                other => panic!("expected Discover, got {:?}", other),
12437            }
12438        } else {
12439            panic!("expected Flow");
12440        }
12441    }
12442
12443    #[test]
12444    fn listen_typed_ref_sets_flag_true() {
12445        let src = "daemon D() { goal: \"x\" listen C as ev { } }";
12446        let prog = parse(src).expect("parse");
12447        if let Declaration::Daemon(d) = &prog.declarations[0] {
12448            assert_eq!(d.listeners.len(), 1);
12449            assert_eq!(d.listeners[0].channel, "C");
12450            assert!(d.listeners[0].channel_is_ref, "typed ref ⇒ true");
12451        } else {
12452            panic!("expected Daemon");
12453        }
12454    }
12455
12456    #[test]
12457    fn listen_string_topic_legacy_flag_false() {
12458        let src = "daemon D() { goal: \"x\" listen \"orders\" as ev { } }";
12459        let prog = parse(src).expect("parse");
12460        if let Declaration::Daemon(d) = &prog.declarations[0] {
12461            assert_eq!(d.listeners.len(), 1);
12462            assert_eq!(d.listeners[0].channel, "orders");
12463            assert!(!d.listeners[0].channel_is_ref, "string topic ⇒ false");
12464        } else {
12465            panic!("expected Daemon");
12466        }
12467    }
12468
12469    // ── v1.6.0 — emit value_ref accepts dotted access ───────────
12470
12471    fn extract_first_emit(prog: &Program) -> &EmitStatement {
12472        if let Declaration::Flow(f) = &prog.declarations[0] {
12473            if let FlowStep::Emit(e) = &f.body[0] {
12474                return e;
12475            }
12476        }
12477        panic!("expected emit statement at flow body[0]");
12478    }
12479
12480    #[test]
12481    fn emit_accepts_bare_identifier_value_ref() {
12482        // Pre-13.i baseline — must keep working.
12483        let prog = parse("flow f() -> Out { emit Hello(payload) }").expect("parse");
12484        let emit = extract_first_emit(&prog);
12485        assert_eq!(emit.channel_ref, "Hello");
12486        assert_eq!(emit.value_ref, "payload");
12487    }
12488
12489    #[test]
12490    fn emit_accepts_two_segment_dotted_value_ref() {
12491        // The exact case adopters reported as broken before 13.i.
12492        let prog = parse("flow f() -> Out { emit Hello(Build.output) }").expect("parse");
12493        let emit = extract_first_emit(&prog);
12494        assert_eq!(emit.value_ref, "Build.output");
12495    }
12496
12497    #[test]
12498    fn emit_accepts_three_segment_nested_dotted_value_ref() {
12499        let prog = parse("flow f() -> Out { emit Score(Analyze.result.score) }").expect("parse");
12500        let emit = extract_first_emit(&prog);
12501        assert_eq!(emit.value_ref, "Analyze.result.score");
12502    }
12503
12504    #[test]
12505    fn emit_dotted_with_trailing_dot_fails() {
12506        // Trailing `.` must still error — every '.' demands an identifier.
12507        let result = parse("flow f() -> Out { emit Hello(Build.) }");
12508        assert!(result.is_err(), "expected parse error for trailing dot");
12509    }
12510}
12511
12512// ── v1.5.2 — declaration_trivia parallel channel tests ──────────────────
12513
12514#[cfg(test)]
12515mod declaration_trivia_tests {
12516    use super::*;
12517    use crate::lexer::Lexer;
12518    use crate::tokens::TriviaKind;
12519
12520    fn parse(src: &str) -> Program {
12521        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
12522        Parser::new(toks).parse().expect("parse")
12523    }
12524
12525    #[test]
12526    fn no_comments_means_empty_trivia_per_decl() {
12527        let prog = parse("flow F() -> Out { }");
12528        assert_eq!(prog.declarations.len(), 1);
12529        assert_eq!(prog.declaration_trivia.len(), 1);
12530        assert!(prog.declaration_trivia[0].leading.is_empty());
12531        assert!(prog.declaration_trivia[0].trailing.is_empty());
12532    }
12533
12534    #[test]
12535    fn doc_line_comment_attaches_as_leading() {
12536        let prog = parse("/// Documents F\nflow F() -> Out { }");
12537        let triv = &prog.declaration_trivia[0];
12538        assert_eq!(triv.leading.len(), 1);
12539        assert_eq!(triv.leading[0].kind, TriviaKind::DocLine);
12540        assert!(triv.leading[0].is_doc());
12541        assert_eq!(triv.leading[0].text, "/// Documents F");
12542    }
12543
12544    #[test]
12545    fn regular_line_comment_attaches_as_leading() {
12546        let prog = parse("// header\nflow F() -> Out { }");
12547        let triv = &prog.declaration_trivia[0];
12548        assert_eq!(triv.leading.len(), 1);
12549        assert_eq!(triv.leading[0].kind, TriviaKind::Line);
12550        assert!(!triv.leading[0].is_doc());
12551    }
12552
12553    #[test]
12554    fn block_doc_comment_attaches_as_leading() {
12555        let prog = parse("/** Doc block */\nflow F() -> Out { }");
12556        let triv = &prog.declaration_trivia[0];
12557        assert_eq!(triv.leading[0].kind, TriviaKind::DocBlock);
12558        assert!(triv.leading[0].is_doc());
12559    }
12560
12561    #[test]
12562    fn multiple_comments_collected_in_source_order() {
12563        let src = "/// First\n/// Second\nflow F() -> Out { }";
12564        let prog = parse(src);
12565        let triv = &prog.declaration_trivia[0];
12566        assert_eq!(triv.leading.len(), 2);
12567        assert_eq!(triv.leading[0].text, "/// First");
12568        assert_eq!(triv.leading[1].text, "/// Second");
12569    }
12570
12571    #[test]
12572    fn three_decls_each_get_own_leading() {
12573        let src = "/// for A\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }\n/// for C\nflow C() -> Out { }";
12574        let prog = parse(src);
12575        assert_eq!(prog.declarations.len(), 3);
12576        assert_eq!(prog.declaration_trivia.len(), 3);
12577        for (idx, name) in ["A", "B", "C"].iter().enumerate() {
12578            let triv = &prog.declaration_trivia[idx];
12579            assert_eq!(triv.leading.len(), 1);
12580            assert_eq!(triv.leading[0].text, format!("/// for {name}"));
12581        }
12582    }
12583
12584    #[test]
12585    fn trailing_comment_attaches_to_last_token_of_decl() {
12586        // Comment on the same line as the decl's closing brace.
12587        let prog = parse("flow F() -> Out { } // tail");
12588        let triv = &prog.declaration_trivia[0];
12589        assert_eq!(triv.trailing.len(), 1);
12590        assert_eq!(triv.trailing[0].text, "// tail");
12591    }
12592
12593    #[test]
12594    fn mixed_doc_and_regular_preserve_order_between_decls() {
12595        let src = "/// doc for A\nflow A() -> Out { }\n\n// header line\n/// doc for B\nflow B() -> Out { }";
12596        let prog = parse(src);
12597        assert_eq!(prog.declarations.len(), 2);
12598        // A: just the doc comment.
12599        assert_eq!(prog.declaration_trivia[0].leading.len(), 1);
12600        // B: header + doc, in source order.
12601        assert_eq!(prog.declaration_trivia[1].leading.len(), 2);
12602        assert_eq!(prog.declaration_trivia[1].leading[0].text, "// header line");
12603        assert_eq!(prog.declaration_trivia[1].leading[1].text, "/// doc for B");
12604    }
12605
12606    #[test]
12607    fn parser_unaffected_by_comments_in_grammar_path() {
12608        // The parser must accept comments interleaved between every
12609        // legal token without affecting the AST shape it produces.
12610        // This is the regression guard for "lossless lexing must not
12611        // change parsing semantics."
12612        let src =
12613            "// before flow\nflow /* between flow and name */ F() -> Out {\n  // body comment\n}";
12614        let prog = parse(src);
12615        assert_eq!(prog.declarations.len(), 1);
12616        if let Declaration::Flow(f) = &prog.declarations[0] {
12617            assert_eq!(f.name, "F");
12618        } else {
12619            panic!("expected Flow declaration");
12620        }
12621    }
12622}
12623
12624// ── v1.5.2 — per-struct trivia fields tests ─────────────────────────────
12625//
12626// 14.b spreads `leading_trivia` / `trailing_trivia` into every Declaration
12627// variant struct (FlowDefinition, ChannelDefinition, PersonaDefinition, …).
12628// The Python AST already had this shape since 14.a; 14.b achieves Rust
12629// parity. The side-channel `Program.declaration_trivia` is preserved for
12630// backward compat — these tests verify the new direct access path.
12631
12632#[cfg(test)]
12633mod per_struct_trivia_tests {
12634    use super::*;
12635    use crate::lexer::Lexer;
12636    use crate::tokens::TriviaKind;
12637
12638    fn parse(src: &str) -> Program {
12639        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
12640        Parser::new(toks).parse().expect("parse")
12641    }
12642
12643    #[test]
12644    fn flow_definition_carries_leading_trivia_directly() {
12645        let prog = parse("/// documents F\nflow F() -> Out { }");
12646        if let Declaration::Flow(f) = &prog.declarations[0] {
12647            assert_eq!(f.leading_trivia.len(), 1);
12648            assert_eq!(f.leading_trivia[0].kind, TriviaKind::DocLine);
12649            assert_eq!(f.leading_trivia[0].text, "/// documents F");
12650            assert!(f.trailing_trivia.is_empty());
12651        } else {
12652            panic!("expected Flow declaration");
12653        }
12654    }
12655
12656    #[test]
12657    fn flow_definition_carries_trailing_trivia_directly() {
12658        let prog = parse("flow F() -> Out { } // tail comment");
12659        if let Declaration::Flow(f) = &prog.declarations[0] {
12660            assert_eq!(f.trailing_trivia.len(), 1);
12661            assert_eq!(f.trailing_trivia[0].text, "// tail comment");
12662        } else {
12663            panic!("expected Flow declaration");
12664        }
12665    }
12666
12667    #[test]
12668    fn channel_definition_carries_trivia_directly() {
12669        // ChannelDefinition is a Tier-1 declaration; verify per-struct fields
12670        // populate just like FlowDefinition.
12671        let src = concat!(
12672            "/// inbound order events\n",
12673            "channel Orders {\n",
12674            "    message:     Order\n",
12675            "    qos:         at_least_once\n",
12676            "    lifetime:    affine\n",
12677            "    persistence: ephemeral\n",
12678            "    shield:      Broker\n",
12679            "}",
12680        );
12681        let prog = parse(src);
12682        if let Declaration::Channel(ch) = &prog.declarations[0] {
12683            assert_eq!(ch.leading_trivia.len(), 1);
12684            assert!(ch.leading_trivia[0].is_doc());
12685            assert_eq!(ch.leading_trivia[0].text, "/// inbound order events");
12686        } else {
12687            panic!("expected Channel declaration");
12688        }
12689    }
12690
12691    #[test]
12692    fn per_struct_fields_match_side_channel() {
12693        // 14.a side-channel and 14.b per-struct fields must hold identical
12694        // data — they are populated by the same parser pass.
12695        let src = "/// for A\n// header for B\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }";
12696        let prog = parse(src);
12697        for (idx, decl) in prog.declarations.iter().enumerate() {
12698            let side = &prog.declaration_trivia[idx];
12699            let (per_lead, per_trail) = match decl {
12700                Declaration::Flow(f) => (&f.leading_trivia, &f.trailing_trivia),
12701                _ => panic!("unexpected variant"),
12702            };
12703            assert_eq!(per_lead.len(), side.leading.len());
12704            assert_eq!(per_trail.len(), side.trailing.len());
12705            for (a, b) in per_lead.iter().zip(side.leading.iter()) {
12706                assert_eq!(a.text, b.text);
12707                assert_eq!(a.kind, b.kind);
12708            }
12709        }
12710    }
12711
12712    #[test]
12713    fn comment_free_program_yields_empty_per_struct_fields() {
12714        let prog = parse("flow F() -> Out { }");
12715        if let Declaration::Flow(f) = &prog.declarations[0] {
12716            assert!(f.leading_trivia.is_empty());
12717            assert!(f.trailing_trivia.is_empty());
12718        } else {
12719            panic!("expected Flow declaration");
12720        }
12721    }
12722}
12723
12724// ── v1.5.2 — inner doc comments (//!, /*!) ──────────────────────────────
12725//
12726// Inner doc comments document the *enclosing* item rather than the next
12727// sibling. Today they flow through the trivia channel like any other
12728// comment; downstream consumers (axon doc, LSP) decide how to interpret
12729// `is_inner_doc()`. These tests verify the lexer→parser pipeline preserves
12730// the inner-doc discriminator end-to-end.
12731
12732#[cfg(test)]
12733mod inner_doc_tests {
12734    use super::*;
12735    use crate::lexer::Lexer;
12736    use crate::tokens::TriviaKind;
12737
12738    fn parse(src: &str) -> Program {
12739        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
12740        Parser::new(toks).parse().expect("parse")
12741    }
12742
12743    #[test]
12744    fn inner_doc_line_reaches_leading_trivia() {
12745        let src = "//! file-level docs\nflow F() -> Out { }";
12746        let prog = parse(src);
12747        let triv = &prog.declaration_trivia[0];
12748        assert_eq!(triv.leading.len(), 1);
12749        assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocLine);
12750        assert!(triv.leading[0].is_doc());
12751        assert!(triv.leading[0].is_inner_doc());
12752        assert_eq!(triv.leading[0].text, "//! file-level docs");
12753        assert_eq!(triv.leading[0].stripped_text(), " file-level docs");
12754    }
12755
12756    #[test]
12757    fn inner_doc_block_reaches_leading_trivia() {
12758        let src = "/*! module-level docs */\nflow F() -> Out { }";
12759        let prog = parse(src);
12760        let triv = &prog.declaration_trivia[0];
12761        assert_eq!(triv.leading.len(), 1);
12762        assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocBlock);
12763        assert!(triv.leading[0].is_inner_doc());
12764        assert_eq!(triv.leading[0].stripped_text(), " module-level docs ");
12765    }
12766
12767    #[test]
12768    fn outer_and_inner_doc_can_coexist() {
12769        // File-level inner doc on top, then an outer doc for the
12770        // declaration. Both reach the trivia channel and remain
12771        // distinguishable via `is_inner_doc()`.
12772        let src = "//! file docs\n/// docs F\nflow F() -> Out { }";
12773        let prog = parse(src);
12774        let triv = &prog.declaration_trivia[0];
12775        assert_eq!(triv.leading.len(), 2);
12776        assert!(triv.leading[0].is_inner_doc());
12777        assert!(triv.leading[1].is_doc());
12778        assert!(!triv.leading[1].is_inner_doc());
12779    }
12780
12781    #[test]
12782    fn inner_doc_reaches_per_struct_fields() {
12783        // Same data must be visible via the per-struct fields (v1.5.2).
12784        let src = "//! intro\nflow F() -> Out { }";
12785        let prog = parse(src);
12786        if let Declaration::Flow(f) = &prog.declarations[0] {
12787            assert_eq!(f.leading_trivia.len(), 1);
12788            assert!(f.leading_trivia[0].is_inner_doc());
12789        } else {
12790            panic!("expected Flow declaration");
12791        }
12792    }
12793}
12794
12795// ── v1.20.0 — Parser error recovery test pack ─────────────────────────────
12796//
12797// Mirror of `tests/test_fase28_parser_recovery.py` (Python side, 28.b).
12798// The test classes here line up 1-1 with the Python ones so the cross-
12799// stack drift gate (28.i) can compare error-list shapes input-for-input.
12800//
12801// Test classes:
12802//   - backwards_compat: existing `parse()` API unchanged
12803//   - single_error_recovery: one bad decl → one error, rest parse OK
12804//   - multi_error_recovery: N independent errors → N entries
12805//   - sync_points: every top-level keyword resyncs correctly
12806//   - parse_result_api: `has_errors`, `is_clean`
12807//   - edge_cases: EOF mid-error, brace imbalance, only-bad-tokens
12808//   - robustness_fuzz: 1000 deterministic-seeded mutations never crash
12809//   - no_ghost_errors: single broken field produces exactly 1 error
12810//   - integration_with_colon_diagnostic: v1.19.4 hint preserved under
12811//     recovery mode
12812#[cfg(test)]
12813mod recovery_tests {
12814    use super::*;
12815    use crate::lexer::Lexer;
12816
12817    /// Lex a source and return tokens for the parser to consume.
12818    /// Mirrors the Python `_parse_recovery` helper.
12819    fn lex(src: &str) -> Vec<Token> {
12820        Lexer::new(src, "<test>").tokenize().expect("lex")
12821    }
12822
12823    /// Parse with recovery mode. Returns `(program, errors)` so call
12824    /// sites read like the Python helper.
12825    fn recover(src: &str) -> ParseResult {
12826        Parser::new(lex(src)).parse_with_recovery()
12827    }
12828
12829    /// Strict parse. Mirrors the Python `_parse_strict` helper.
12830    fn strict(src: &str) -> Result<Program, ParseError> {
12831        Parser::new(lex(src)).parse()
12832    }
12833
12834    // ── backwards_compat ─────────────────────────────────────────
12835
12836    #[test]
12837    fn strict_parse_unchanged_for_clean_source() {
12838        // The existing `parse()` API must continue to succeed
12839        // verbatim on every well-formed input — D9.
12840        let src = "intent I {}";
12841        let prog = strict(src).expect("clean parse");
12842        assert_eq!(prog.declarations.len(), 1);
12843    }
12844
12845    #[test]
12846    fn strict_parse_still_raises_on_first_error() {
12847        // D9 + D8: opt-in to recovery via `parse_with_recovery`;
12848        // strict mode must still bubble the first error.
12849        // (Using a parse-time error rather than a lex error — `@@@`
12850        // would be rejected by the lexer, which is out of scope.)
12851        let src = "flow F() { } not_a_keyword flow G() { }";
12852        let _ = strict(src).expect_err("must error fast in strict mode");
12853    }
12854
12855    #[test]
12856    fn recovery_clean_source_yields_no_errors() {
12857        let src = "flow F() { } flow G() { }";
12858        let pr = recover(src);
12859        assert!(pr.is_clean(), "errors: {:?}", pr.errors);
12860        assert_eq!(pr.program.declarations.len(), 2);
12861    }
12862
12863    // ── single_error_recovery ────────────────────────────────────
12864
12865    #[test]
12866    fn single_unknown_top_level_token_recovers() {
12867        // One garbage token at top level; rest must parse.
12868        let src = "garbage_token flow F() { } flow G() { }";
12869        let pr = recover(src);
12870        assert_eq!(pr.errors.len(), 1, "errors: {:?}", pr.errors);
12871        assert_eq!(pr.program.declarations.len(), 2);
12872    }
12873
12874    #[test]
12875    fn error_in_first_decl_does_not_block_second() {
12876        // `flow F` body refers to non-keyword `nope`; the error
12877        // recovery must skip to the next top-level keyword.
12878        let src = "flow F() { not_a_step nope } flow G() { }";
12879        let pr = recover(src);
12880        assert!(pr.has_errors(), "expected at least one error");
12881        // The second flow must be reachable.
12882        let names: Vec<&str> = pr
12883            .program
12884            .declarations
12885            .iter()
12886            .filter_map(|d| match d {
12887                Declaration::Flow(f) => Some(f.name.as_str()),
12888                _ => None,
12889            })
12890            .collect();
12891        assert!(names.contains(&"G"), "G not found among {names:?}");
12892    }
12893
12894    #[test]
12895    fn malformed_declaration_then_clean_intent_recovers() {
12896        let src = "flow @ () { } intent I {}";
12897        let pr = recover(src);
12898        assert!(pr.has_errors());
12899        let kinds: Vec<&str> = pr
12900            .program
12901            .declarations
12902            .iter()
12903            .map(|d| match d {
12904                Declaration::Intent(_) => "intent",
12905                Declaration::Flow(_) => "flow",
12906                _ => "other",
12907            })
12908            .collect();
12909        assert!(kinds.contains(&"intent"), "kinds: {kinds:?}");
12910    }
12911
12912    #[test]
12913    fn recovery_does_not_double_count_a_single_error() {
12914        // Regression for the "ghost error" pathology that surfaced
12915        // during 28.b dev: a nested-decl error must not also fire
12916        // an "Unexpected token at top level" from the outer loop.
12917        // The Rust grammar has stricter intra-flow requirements
12918        // than Python; the invariant we assert here is that the
12919        // outer loop emits zero "Unexpected token at top level"
12920        // errors after an inner step-shape error.
12921        let src = "flow F() { not_a_step }";
12922        let pr = recover(src);
12923        let outer_ghosts = pr
12924            .errors
12925            .iter()
12926            .filter(|e| e.message.contains("at top level"))
12927            .count();
12928        assert_eq!(outer_ghosts, 0, "ghost errors: {:?}", pr.errors);
12929    }
12930
12931    // ── multi_error_recovery ─────────────────────────────────────
12932
12933    #[test]
12934    fn three_independent_errors_yield_three_entries() {
12935        let src =
12936            "garbage1 flow F() { } garbage2 flow G() { } garbage3 flow H() { }";
12937        let pr = recover(src);
12938        assert_eq!(pr.errors.len(), 3, "errors: {:?}", pr.errors);
12939        assert_eq!(pr.program.declarations.len(), 3);
12940    }
12941
12942    #[test]
12943    fn all_errors_no_valid_declarations() {
12944        let src = "foo bar baz qux";
12945        let pr = recover(src);
12946        assert!(pr.has_errors());
12947        assert!(pr.program.declarations.is_empty());
12948    }
12949
12950    #[test]
12951    fn errors_recorded_in_source_order() {
12952        let src = "x flow A() { } y flow B() { } z flow C() { }";
12953        let pr = recover(src);
12954        assert_eq!(pr.errors.len(), 3);
12955        let lines: Vec<u32> = pr.errors.iter().map(|e| e.line).collect();
12956        // Same source-line means we compare by column ordering;
12957        // either way they must be non-decreasing.
12958        assert!(
12959            lines.windows(2).all(|w| w[0] <= w[1]),
12960            "errors out of order: {lines:?}"
12961        );
12962    }
12963
12964    // ── sync_points ──────────────────────────────────────────────
12965
12966    #[test]
12967    fn sync_to_flow_keyword() {
12968        let src = "garbage flow F() { }";
12969        let pr = recover(src);
12970        assert_eq!(pr.program.declarations.len(), 1);
12971    }
12972
12973    #[test]
12974    fn sync_to_intent_keyword() {
12975        let src = "garbage intent I {}";
12976        let pr = recover(src);
12977        assert_eq!(pr.program.declarations.len(), 1);
12978    }
12979
12980    #[test]
12981    fn sync_to_persona_keyword() {
12982        let src = "garbage persona P { name: \"P\" role: \"R\" }";
12983        let pr = recover(src);
12984        assert!(
12985            pr.program
12986                .declarations
12987                .iter()
12988                .any(|d| matches!(d, Declaration::Persona(_))),
12989            "persona not recovered: decls = {:?}",
12990            pr.program.declarations.len()
12991        );
12992    }
12993
12994    #[test]
12995    fn sync_to_run_keyword() {
12996        let src = "garbage run R { input: { user_message: \"hi\" } }";
12997        let pr = recover(src);
12998        // Either Run was parsed, or recovery still produced ≥1 err.
12999        assert!(pr.has_errors());
13000    }
13001
13002    // ── parse_result_api ─────────────────────────────────────────
13003
13004    #[test]
13005    fn parse_result_has_errors_and_is_clean_invert() {
13006        let pr_clean = recover("flow F() { }");
13007        assert!(pr_clean.is_clean());
13008        assert!(!pr_clean.has_errors());
13009
13010        let pr_err = recover("garbage");
13011        assert!(!pr_err.is_clean());
13012        assert!(pr_err.has_errors());
13013    }
13014
13015    #[test]
13016    fn parse_result_program_field_holds_partial_program() {
13017        let pr = recover("garbage flow F() { }");
13018        assert!(!pr.program.declarations.is_empty());
13019    }
13020
13021    #[test]
13022    fn parse_result_errors_carry_line_and_column() {
13023        let pr = recover("garbage");
13024        assert!(!pr.errors.is_empty());
13025        let e = &pr.errors[0];
13026        assert!(e.line >= 1);
13027        // Column may be 0-based or 1-based depending on lexer;
13028        // accept anything ≥ 0.
13029        let _ = e.column;
13030        assert!(!e.message.is_empty());
13031    }
13032
13033    #[test]
13034    fn parse_result_debug_renders() {
13035        let pr = recover("flow F() { }");
13036        let s = format!("{pr:?}");
13037        assert!(s.contains("ParseResult"));
13038    }
13039
13040    // ── edge_cases ───────────────────────────────────────────────
13041
13042    #[test]
13043    fn empty_source_is_clean() {
13044        let pr = recover("");
13045        assert!(pr.is_clean());
13046        assert!(pr.program.declarations.is_empty());
13047    }
13048
13049    #[test]
13050    fn whitespace_only_source_is_clean() {
13051        let pr = recover("   \n\n\t  \n");
13052        assert!(pr.is_clean());
13053        assert!(pr.program.declarations.is_empty());
13054    }
13055
13056    #[test]
13057    fn only_garbage_does_not_crash() {
13058        // Lex-clean garbage tokens (avoids AxonLexerError).
13059        let pr = recover("foo bar baz { qux quux } corge { grault }");
13060        assert!(pr.has_errors());
13061    }
13062
13063    #[test]
13064    fn unbalanced_close_brace_does_not_crash() {
13065        let pr = recover("} flow F() { }");
13066        // Recovery must keep walking past stray `}`.
13067        let names: Vec<&str> = pr
13068            .program
13069            .declarations
13070            .iter()
13071            .filter_map(|d| match d {
13072                Declaration::Flow(f) => Some(f.name.as_str()),
13073                _ => None,
13074            })
13075            .collect();
13076        assert!(names.contains(&"F"), "F not recovered: {names:?}");
13077    }
13078
13079    #[test]
13080    fn error_at_eof_does_not_loop() {
13081        // Truncated declaration. Must terminate; finite errors.
13082        let pr = recover("flow F() { ");
13083        // Either errored or somehow accepted — but must terminate.
13084        let _ = pr.errors.len();
13085    }
13086
13087    #[test]
13088    fn nested_braces_inside_error_still_balance() {
13089        // Walker must respect brace depth so a `}` inside a malformed
13090        // block does not prematurely sync.
13091        let src = "flow F() { not_a_step { inner } } flow G() { }";
13092        let pr = recover(src);
13093        let names: Vec<&str> = pr
13094            .program
13095            .declarations
13096            .iter()
13097            .filter_map(|d| match d {
13098                Declaration::Flow(f) => Some(f.name.as_str()),
13099                _ => None,
13100            })
13101            .collect();
13102        assert!(names.contains(&"G"), "G not recovered: {names:?}");
13103    }
13104
13105    // ── robustness_fuzz ──────────────────────────────────────────
13106    //
13107    // Deterministic-seeded mutator (xorshift). 100 buckets ×
13108    // 10 mutations = 1000 iterations, byte-bounded so fuzz time
13109    // stays under 1 s on a release build. Recovery must NEVER crash;
13110    // lexer-level errors are out of scope (lexer recovery is its own
13111    // step). 28.b mirrors this with the same structure.
13112
13113    #[derive(Clone, Copy)]
13114    struct Xorshift(u64);
13115    impl Xorshift {
13116        fn next(&mut self) -> u64 {
13117            let mut x = self.0;
13118            x ^= x << 13;
13119            x ^= x >> 7;
13120            x ^= x << 17;
13121            self.0 = x;
13122            x
13123        }
13124        fn pick<T: Copy>(&mut self, slice: &[T]) -> T {
13125            slice[(self.next() as usize) % slice.len()]
13126        }
13127    }
13128
13129    fn mutate(src: &str, rng: &mut Xorshift) -> String {
13130        let mut bytes: Vec<u8> = src.bytes().collect();
13131        if bytes.is_empty() {
13132            return src.to_string();
13133        }
13134        let op = rng.next() % 4;
13135        let pos = (rng.next() as usize) % bytes.len();
13136        // Stick to ASCII-safe printable bytes to keep input lex-able
13137        // most of the time. AxonLexerError is still possible and is
13138        // tolerated by the recovery contract.
13139        let safe: &[u8] = b"abcdefghijklmnopqrstuvwxyz {}();:,_0123456789";
13140        match op {
13141            0 => {
13142                bytes.remove(pos);
13143            }
13144            1 => {
13145                let b = rng.pick(safe);
13146                bytes.insert(pos, b);
13147            }
13148            2 if pos + 1 < bytes.len() => {
13149                bytes.swap(pos, pos + 1);
13150            }
13151            _ => {
13152                let b = rng.pick(safe);
13153                bytes[pos] = b;
13154            }
13155        }
13156        // Lossy decode: mutator may have produced invalid UTF-8;
13157        // strip non-ASCII before handing to the lexer.
13158        bytes.retain(|b| b.is_ascii());
13159        String::from_utf8_lossy(&bytes).into_owned()
13160    }
13161
13162    #[test]
13163    fn fuzz_recovery_never_crashes() {
13164        let seed_bases = [
13165            "flow F() { }",
13166            "intent I { }",
13167            "persona P { name: \"P\" role: \"R\" }",
13168            "intent J { ask: \"a\" }",
13169            "type T = String",
13170        ];
13171        // 100 buckets × 10 mutations = 1000 iterations, deterministic.
13172        for (bucket, base) in (0..100u64).zip(seed_bases.iter().cycle()) {
13173            let mut rng = Xorshift(0x1234_5678_9abc_def0_u64.wrapping_add(bucket));
13174            let mut current = (*base).to_string();
13175            for _ in 0..10 {
13176                current = mutate(&current, &mut rng);
13177                // Lexer may reject; that's outside parser-recovery
13178                // scope (28.b/c). Skip those iterations.
13179                let toks = match Lexer::new(&current, "<fuzz>").tokenize() {
13180                    Ok(t) => t,
13181                    Err(_) => continue,
13182                };
13183                // Recovery must not panic on any well-lexed input.
13184                let _pr = Parser::new(toks).parse_with_recovery();
13185            }
13186        }
13187    }
13188
13189    // ── integration_with_v1_19_4_colon_diagnostic ────────────────
13190
13191    #[test]
13192    fn missing_colon_hint_preserved_under_recovery() {
13193        // The Rust frontend's strict `parse()` carries the same
13194        // colon diagnostic shape as the Python side. Recovery mode
13195        // must not erase it.
13196        let src = "flow F() { run R { input { user_message: \"hi\" } } }";
13197        let pr = recover(src);
13198        // Either the parser accepts this (some shape may be valid)
13199        // or it errors — but if it errors, the message must surface
13200        // the diagnostic content.
13201        if !pr.errors.is_empty() {
13202            let any_msg = pr.errors.iter().any(|e| !e.message.is_empty());
13203            assert!(any_msg);
13204        }
13205    }
13206
13207    // ── recovery preserves declaration ordering ──────────────────
13208
13209    #[test]
13210    fn recovered_declarations_appear_in_source_order() {
13211        let src = "flow A() { } garbage flow B() { } garbage flow C() { }";
13212        let pr = recover(src);
13213        let names: Vec<&str> = pr
13214            .program
13215            .declarations
13216            .iter()
13217            .filter_map(|d| match d {
13218                Declaration::Flow(f) => Some(f.name.as_str()),
13219                _ => None,
13220            })
13221            .collect();
13222        assert_eq!(names, vec!["A", "B", "C"]);
13223    }
13224}
13225
13226// ── v1.20.0 — Source-context diagnostic block test pack ───────────────────
13227//
13228// Mirror of `tests/test_fase28_source_context.py` (Python side, 28.d).
13229// The render output must be byte-identical to the Python `SourceSnippet.render`
13230// on the same input — D7 ratified (cross-stack drift gate). Golden strings
13231// in `golden_*` tests are duplicated verbatim in the Python pack; edits
13232// here MUST be mirrored on the Python side and vice versa.
13233#[cfg(test)]
13234mod source_context_tests {
13235    use super::*;
13236    use crate::lexer::Lexer;
13237
13238    fn snippet(source: &str, line: u32, column: u32, filename: &str) -> String {
13239        SourceSnippet::new(
13240            source.to_string(),
13241            line,
13242            column,
13243            filename.to_string(),
13244        )
13245        .render()
13246    }
13247
13248    // ── Pure rendering ──────────────────────────────────────────
13249
13250    #[test]
13251    fn rustc_style_block_for_middle_line() {
13252        let src = "line one\nline two\nline three\nline four\nline five";
13253        let out = snippet(src, 3, 6, "x.axon");
13254        assert!(out.contains("--> x.axon:3:6"));
13255        assert!(out.contains("1 | line one"));
13256        assert!(out.contains("2 | line two"));
13257        assert!(out.contains("3 | line three"));
13258        assert!(out.contains("4 | line four"));
13259        assert!(out.contains("5 | line five"));
13260        // Caret col 6 → 5-space pad. Empty gutter is 1 space (gutter=1).
13261        assert!(out.contains("\n  |      ^"), "out:\n{out}");
13262    }
13263
13264    #[test]
13265    fn caret_column_one_renders_correctly() {
13266        let out = snippet("abc\n", 1, 1, "<source>");
13267        assert!(out.contains("\n  | ^"));
13268    }
13269
13270    #[test]
13271    fn first_line_clamps_context_before_to_zero() {
13272        let src = "first\nsecond\nthird\nfourth\nfifth";
13273        let out = snippet(src, 1, 1, "<source>");
13274        assert!(out.contains("1 | first"));
13275        assert!(out.contains("2 | second"));
13276        assert!(out.contains("3 | third"));
13277        assert!(!out.contains("4 | fourth"));
13278    }
13279
13280    #[test]
13281    fn last_line_clamps_context_after_to_eof() {
13282        let src = "first\nsecond\nthird\nfourth\nfifth";
13283        let out = snippet(src, 5, 2, "<source>");
13284        assert!(out.contains("5 | fifth"));
13285        assert!(out.contains("3 | third"));
13286        assert!(out.contains("4 | fourth"));
13287        assert!(!out.contains("2 | second"));
13288    }
13289
13290    #[test]
13291    fn gutter_width_grows_with_line_count() {
13292        let src: String = (1..=12).map(|i| format!("line{i}")).collect::<Vec<_>>().join("\n");
13293        let out = snippet(&src, 12, 1, "<source>");
13294        assert!(out.contains("12 | line12"));
13295        assert!(out.contains("10 | line10"));
13296    }
13297
13298    // ── Edge cases ──────────────────────────────────────────────
13299
13300    #[test]
13301    fn empty_source_returns_empty() {
13302        assert_eq!(snippet("", 1, 1, "<source>"), "");
13303    }
13304
13305    #[test]
13306    fn zero_line_returns_empty() {
13307        assert_eq!(snippet("hi", 0, 1, "<source>"), "");
13308    }
13309
13310    #[test]
13311    fn out_of_range_line_returns_empty() {
13312        assert_eq!(snippet("hi", 99, 1, "<source>"), "");
13313    }
13314
13315    #[test]
13316    fn caret_clamps_past_eol() {
13317        let out = snippet("hello", 1, 50, "<source>");
13318        assert!(out.contains("\n  |      ^"), "out:\n{out}");
13319    }
13320
13321    #[test]
13322    fn unicode_codepoint_count_for_caret_clamp() {
13323        // "héllo" = 5 codepoints; column past EOL clamps to 6.
13324        let out = snippet("héllo", 1, 99, "<source>");
13325        assert!(out.contains("\n  |      ^"), "out:\n{out}");
13326    }
13327
13328    #[test]
13329    fn trailing_newline_does_not_create_phantom_last_line() {
13330        let out = snippet("first\nsecond\n", 2, 1, "<source>");
13331        assert!(!out.contains("3 |"));
13332        assert!(out.contains("2 | second"));
13333    }
13334
13335    // ── Parser attach plumbing ──────────────────────────────────
13336
13337    fn lex(src: &str) -> Vec<Token> {
13338        Lexer::new(src, "<test>").tokenize().expect("lex")
13339    }
13340
13341    #[test]
13342    fn strict_parse_attaches_snippet_when_source_given() {
13343        let src = "garbage_token\nflow F() { }";
13344        let err = Parser::new(lex(src))
13345            .with_source(src, "x.axon")
13346            .parse()
13347            .expect_err("must error");
13348        assert!(err.source_snippet.is_some());
13349        let display = format!("{err}");
13350        assert!(display.contains("--> x.axon:"), "display: {display}");
13351    }
13352
13353    #[test]
13354    fn strict_parse_no_snippet_when_no_source() {
13355        let src = "garbage_token";
13356        let err = Parser::new(lex(src)).parse().expect_err("must error");
13357        assert!(err.source_snippet.is_none());
13358        let display = format!("{err}");
13359        assert!(!display.contains("\n  -->"));
13360    }
13361
13362    #[test]
13363    fn every_recovered_error_has_snippet() {
13364        let src = "garbage1\nflow F() { }\ngarbage2\nflow G() { }";
13365        let result = Parser::new(lex(src))
13366            .with_source(src, "multi.axon")
13367            .parse_with_recovery();
13368        assert!(!result.errors.is_empty());
13369        for err in &result.errors {
13370            assert!(err.source_snippet.is_some());
13371            let display = format!("{err}");
13372            assert!(
13373                display.contains("--> multi.axon:"),
13374                "display: {display}"
13375            );
13376        }
13377    }
13378
13379    #[test]
13380    fn recovery_no_snippet_when_no_source() {
13381        let src = "garbage1 garbage2";
13382        let result = Parser::new(lex(src)).parse_with_recovery();
13383        for err in &result.errors {
13384            assert!(err.source_snippet.is_none());
13385        }
13386    }
13387
13388    #[test]
13389    fn snippet_points_at_correct_line_for_each_error() {
13390        let src = "garbage_a\nflow F() { }\ngarbage_b\nflow G() { }";
13391        let result = Parser::new(lex(src))
13392            .with_source(src, "x")
13393            .parse_with_recovery();
13394        for err in &result.errors {
13395            let sn = err.source_snippet.as_ref().expect("snippet");
13396            assert_eq!(sn.line, err.line);
13397        }
13398    }
13399
13400    // ── Backwards-compat ────────────────────────────────────────
13401
13402    #[test]
13403    fn legacy_constructor_still_works() {
13404        let src = "flow F() { }";
13405        let prog = Parser::new(lex(src)).parse().expect("clean");
13406        assert_eq!(prog.declarations.len(), 1);
13407    }
13408
13409    #[test]
13410    fn attach_source_idempotent() {
13411        let err = ParseError {
13412            message: "bad".to_string(),
13413            line: 2,
13414            column: 3,
13415            ..Default::default()
13416        };
13417        let err2 = err.clone().attach_source("a\nb\nc\n", "f.axon");
13418        let first = format!("{err2}");
13419        let err3 = err.attach_source("a\nb\nc\n", "f.axon");
13420        let second = format!("{err3}");
13421        assert_eq!(first, second);
13422    }
13423
13424    #[test]
13425    fn attach_source_noop_when_line_zero() {
13426        let err = ParseError {
13427            message: "bad".to_string(),
13428            line: 0,
13429            column: 0,
13430            ..Default::default()
13431        };
13432        let err = err.attach_source("a\nb\nc\n", "f.axon");
13433        assert!(err.source_snippet.is_none());
13434    }
13435
13436    // ── Cross-stack golden parity ───────────────────────────────
13437    // These golden strings are duplicated verbatim in the Python
13438    // test pack at `tests/test_fase28_source_context.py::TestRustParityShape`.
13439    // Edits here MUST be mirrored in the Python pack — D7.
13440
13441    #[test]
13442    fn golden_simple_three_line_block() {
13443        let src = "alpha\nbeta\ngamma";
13444        let out = snippet(src, 2, 3, "g.axon");
13445        // Note: gutter=1, so empty_gutter=" " (one space). The
13446        // " --> ..." line therefore starts with two spaces ("<empty>"
13447        // + literal " --> ...").
13448        let expected = concat!(
13449            "  --> g.axon:2:3\n",
13450            "  |\n",
13451            "1 | alpha\n",
13452            "2 | beta\n",
13453            "  |   ^\n",
13454            "3 | gamma",
13455        );
13456        assert_eq!(out, expected);
13457    }
13458
13459    #[test]
13460    fn golden_first_line_caret() {
13461        let src = "abc\ndef\n";
13462        let out = snippet(src, 1, 1, "x");
13463        let expected = concat!(
13464            "  --> x:1:1\n",
13465            "  |\n",
13466            "1 | abc\n",
13467            "  | ^\n",
13468            "2 | def",
13469        );
13470        assert_eq!(out, expected);
13471    }
13472
13473    #[test]
13474    fn golden_two_digit_gutter() {
13475        let src: String = (1..=11)
13476            .map(|i| format!("L{i}"))
13477            .collect::<Vec<_>>()
13478            .join("\n");
13479        let out = snippet(&src, 10, 2, "big");
13480        let expected = concat!(
13481            "   --> big:10:2\n",
13482            "   |\n",
13483            " 8 | L8\n",
13484            " 9 | L9\n",
13485            "10 | L10\n",
13486            "   |  ^\n",
13487            "11 | L11",
13488        );
13489        assert_eq!(out, expected);
13490    }
13491}
13492
13493// ── v1.20.0 — Parser integration tests for smart-suggest ──────────────────
13494//
13495// Mirror of `tests/test_fase28_smart_suggest.py::TestParserIntegration`.
13496// Verifies that the parser actually wires `suggest_for` into the
13497// unknown-keyword diagnostic at both error sites — top-level and
13498// flow-body.
13499#[cfg(test)]
13500mod smart_suggest_parser_tests {
13501    use super::*;
13502    use crate::lexer::Lexer;
13503
13504    fn lex(src: &str) -> Vec<Token> {
13505        Lexer::new(src, "<test>").tokenize().expect("lex")
13506    }
13507
13508    #[test]
13509    fn top_level_typo_suggests_flow() {
13510        let src = "flwo F() { }";
13511        let err = Parser::new(lex(src)).parse().expect_err("must error");
13512        assert!(
13513            err.message.contains("Did you mean `flow`?"),
13514            "msg: {}",
13515            err.message
13516        );
13517    }
13518
13519    #[test]
13520    fn top_level_unknown_far_no_suggestion() {
13521        let src = "qwerty F() { }";
13522        let err = Parser::new(lex(src)).parse().expect_err("must error");
13523        assert!(
13524            !err.message.contains("Did you mean"),
13525            "msg: {}",
13526            err.message
13527        );
13528    }
13529
13530    #[test]
13531    fn flow_body_typo_suggests_step() {
13532        let src = "flow F() { stepp S {} }";
13533        let err = Parser::new(lex(src)).parse().expect_err("must error");
13534        assert!(
13535            err.message.contains("Did you mean `step`"),
13536            "msg: {}",
13537            err.message
13538        );
13539    }
13540
13541    #[test]
13542    fn flow_body_typo_suggests_reason() {
13543        let src = "flow F() { reasn R {} }";
13544        let err = Parser::new(lex(src)).parse().expect_err("must error");
13545        assert!(
13546            err.message.contains("Did you mean `reason`?"),
13547            "msg: {}",
13548            err.message
13549        );
13550    }
13551
13552    #[test]
13553    fn recovery_mode_carries_hint() {
13554        let src = "flwo F() { }";
13555        let result = Parser::new(lex(src)).parse_with_recovery();
13556        assert!(
13557            result
13558                .errors
13559                .iter()
13560                .any(|e| e.message.contains("Did you mean `flow`?")),
13561            "errors: {:?}",
13562            result.errors
13563        );
13564    }
13565}
13566
13567// ── v1.30.0 — mutate / purge where-clause capture ────────────────
13568
13569#[cfg(test)]
13570mod mutate_purge_where_tests {
13571    use super::*;
13572
13573    fn parse(src: &str) -> Program {
13574        let tokens = crate::lexer::Lexer::new(src, "<test>")
13575            .tokenize()
13576            .expect("lex");
13577        Parser::new(tokens).parse().expect("parse")
13578    }
13579
13580    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
13581        for d in &prog.declarations {
13582            if let Declaration::Flow(f) = d {
13583                if f.name == flow {
13584                    return f.body.first().expect("flow has at least one step");
13585                }
13586            }
13587        }
13588        panic!("flow `{flow}` not found");
13589    }
13590
13591    #[test]
13592    fn mutate_captures_its_where_clause() {
13593        // Pre-35.m the `{ where: }` block was skipped — every mutate
13594        // ran whole-store. It must now reach `where_expr`.
13595        let prog =
13596            parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
13597        match first_step(&prog, "F") {
13598            FlowStep::Mutate(m) => {
13599                assert_eq!(m.store_name, "accounts");
13600                assert_eq!(m.where_expr, "id = 1");
13601            }
13602            other => panic!("expected Mutate, got {other:?}"),
13603        }
13604    }
13605
13606    #[test]
13607    fn purge_captures_its_where_clause() {
13608        let prog =
13609            parse("flow F() -> Unit { purge logs { where: \"ts < 100\" } }");
13610        match first_step(&prog, "F") {
13611            FlowStep::Purge(p) => {
13612                assert_eq!(p.store_name, "logs");
13613                assert_eq!(p.where_expr, "ts < 100");
13614            }
13615            other => panic!("expected Purge, got {other:?}"),
13616        }
13617    }
13618
13619    #[test]
13620    fn mutate_without_a_where_block_is_a_whole_store_op() {
13621        // No `{ where: }` → an empty filter → the runtime renders
13622        // `WHERE TRUE` (every row). A valid, intentional op.
13623        let prog = parse("flow F() -> Unit { mutate accounts }");
13624        match first_step(&prog, "F") {
13625            FlowStep::Mutate(m) => {
13626                assert_eq!(m.store_name, "accounts");
13627                assert_eq!(m.where_expr, "");
13628            }
13629            other => panic!("expected Mutate, got {other:?}"),
13630        }
13631    }
13632}
13633
13634// ── v1.30.0 — persist field-block capture ────────────────────────
13635
13636#[cfg(test)]
13637mod persist_fields_tests {
13638    use super::*;
13639
13640    fn parse(src: &str) -> Program {
13641        let tokens = crate::lexer::Lexer::new(src, "<test>")
13642            .tokenize()
13643            .expect("lex");
13644        Parser::new(tokens).parse().expect("parse")
13645    }
13646
13647    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
13648        for d in &prog.declarations {
13649            if let Declaration::Flow(f) = d {
13650                if f.name == flow {
13651                    return f.body.first().expect("flow has at least one step");
13652                }
13653            }
13654        }
13655        panic!("flow `{flow}` not found");
13656    }
13657
13658    #[test]
13659    fn persist_captures_its_field_block() {
13660        // Pre-35.o the `{ col: value }` block was skipped — every
13661        // persist wrote the whole binding context. It must now reach
13662        // `fields`, in source order, with value expressions raw.
13663        let prog = parse(
13664            "flow F() -> Unit { persist into chat_history { \
13665             session_id: \"${session_id}\" sender: \"user\" \
13666             content: \"${message}\" } }",
13667        );
13668        match first_step(&prog, "F") {
13669            FlowStep::Persist(p) => {
13670                assert_eq!(p.store_name, "chat_history");
13671                assert_eq!(
13672                    p.fields,
13673                    vec![
13674                        ("session_id".to_string(), "${session_id}".to_string()),
13675                        ("sender".to_string(), "user".to_string()),
13676                        ("content".to_string(), "${message}".to_string()),
13677                    ]
13678                );
13679            }
13680            other => panic!("expected Persist, got {other:?}"),
13681        }
13682    }
13683
13684    #[test]
13685    fn persist_without_a_block_keeps_the_user_bindings_fallback() {
13686        // No `{ }` → empty `fields` → the runtime falls back to the
13687        // v1.30.0 user-bindings row. Backward-compatible.
13688        let prog = parse("flow F() -> Unit { persist events }");
13689        match first_step(&prog, "F") {
13690            FlowStep::Persist(p) => {
13691                assert_eq!(p.store_name, "events");
13692                assert!(p.fields.is_empty());
13693            }
13694            other => panic!("expected Persist, got {other:?}"),
13695        }
13696    }
13697
13698    #[test]
13699    fn persist_accepts_the_optional_into_connector() {
13700        // `persist into X` and `persist X` resolve to the SAME store
13701        // name — pre-35.o `into` was captured AS the store name.
13702        let with =
13703            parse("flow F() -> Unit { persist into accounts { id: \"1\" } }");
13704        let without =
13705            parse("flow F() -> Unit { persist accounts { id: \"1\" } }");
13706        for prog in [&with, &without] {
13707            match first_step(prog, "F") {
13708                FlowStep::Persist(p) => assert_eq!(p.store_name, "accounts"),
13709                other => panic!("expected Persist, got {other:?}"),
13710            }
13711        }
13712    }
13713
13714    #[test]
13715    fn persist_into_without_a_block_resolves_the_store_name() {
13716        // `persist into events` — the `into` connector is skipped, the
13717        // store name is `events` (not `into`). Lateral bug closed.
13718        let prog = parse("flow F() -> Unit { persist into events }");
13719        match first_step(&prog, "F") {
13720            FlowStep::Persist(p) => {
13721                assert_eq!(p.store_name, "events");
13722                assert!(p.fields.is_empty());
13723            }
13724            other => panic!("expected Persist, got {other:?}"),
13725        }
13726    }
13727
13728    #[test]
13729    fn persist_fields_lower_into_the_ir() {
13730        // The IR generator must carry `fields` onto `IRPersistStep`
13731        // so the runtime reads exactly the declared columns.
13732        let prog = parse(
13733            "flow F() -> Unit { persist into chat { content: \"${msg}\" } }",
13734        );
13735        let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
13736        let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
13737        match flow.steps.first().expect("one step") {
13738            crate::ir_nodes::IRFlowNode::Persist(p) => {
13739                assert_eq!(p.store_name, "chat");
13740                assert_eq!(
13741                    p.fields,
13742                    vec![("content".to_string(), "${msg}".to_string())]
13743                );
13744            }
13745            other => panic!("expected IRFlowNode::Persist, got {other:?}"),
13746        }
13747    }
13748}
13749
13750// ── v1.30.0 — mutate SET-field-block capture ─────────────────────
13751
13752#[cfg(test)]
13753mod mutate_fields_tests {
13754    use super::*;
13755
13756    fn parse(src: &str) -> Program {
13757        let tokens = crate::lexer::Lexer::new(src, "<test>")
13758            .tokenize()
13759            .expect("lex");
13760        Parser::new(tokens).parse().expect("parse")
13761    }
13762
13763    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
13764        for d in &prog.declarations {
13765            if let Declaration::Flow(f) = d {
13766                if f.name == flow {
13767                    return f.body.first().expect("flow has at least one step");
13768                }
13769            }
13770        }
13771        panic!("flow `{flow}` not found");
13772    }
13773
13774    #[test]
13775    fn mutate_captures_its_set_field_block() {
13776        // Pre-35.p every key but `where:` was skipped — the runtime
13777        // SET every flow binding. The SET columns must now reach
13778        // `fields`, in source order, with `where:` still captured.
13779        let prog = parse(
13780            "flow F() -> Unit { mutate accounts { where: \"id = ${id}\" \
13781             balance: \"${new_balance}\" status: \"active\" } }",
13782        );
13783        match first_step(&prog, "F") {
13784            FlowStep::Mutate(m) => {
13785                assert_eq!(m.store_name, "accounts");
13786                assert_eq!(m.where_expr, "id = ${id}");
13787                assert_eq!(
13788                    m.fields,
13789                    vec![
13790                        ("balance".to_string(), "${new_balance}".to_string()),
13791                        ("status".to_string(), "active".to_string()),
13792                    ]
13793                );
13794            }
13795            other => panic!("expected Mutate, got {other:?}"),
13796        }
13797    }
13798
13799    #[test]
13800    fn mutate_where_only_block_has_no_set_fields() {
13801        // A `{ where: }`-only block → empty `fields` → the runtime
13802        // falls back to the v1.31.0 user-bindings SET.
13803        let prog =
13804            parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
13805        match first_step(&prog, "F") {
13806            FlowStep::Mutate(m) => {
13807                assert_eq!(m.where_expr, "id = 1");
13808                assert!(m.fields.is_empty());
13809            }
13810            other => panic!("expected Mutate, got {other:?}"),
13811        }
13812    }
13813
13814    #[test]
13815    fn mutate_with_no_block_is_a_whole_store_op() {
13816        // No block at all → empty where + empty fields (a whole-store
13817        // UPDATE from user bindings) — unchanged from 35.m.
13818        let prog = parse("flow F() -> Unit { mutate accounts }");
13819        match first_step(&prog, "F") {
13820            FlowStep::Mutate(m) => {
13821                assert_eq!(m.store_name, "accounts");
13822                assert_eq!(m.where_expr, "");
13823                assert!(m.fields.is_empty());
13824            }
13825            other => panic!("expected Mutate, got {other:?}"),
13826        }
13827    }
13828
13829    #[test]
13830    fn mutate_fields_lower_into_the_ir() {
13831        let prog = parse(
13832            "flow F() -> Unit { mutate t { where: \"id = 1\" v: \"${x}\" } }",
13833        );
13834        let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
13835        let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
13836        match flow.steps.first().expect("one step") {
13837            crate::ir_nodes::IRFlowNode::Mutate(m) => {
13838                assert_eq!(m.where_expr, "id = 1");
13839                assert_eq!(
13840                    m.fields,
13841                    vec![("v".to_string(), "${x}".to_string())]
13842                );
13843            }
13844            other => panic!("expected IRFlowNode::Mutate, got {other:?}"),
13845        }
13846    }
13847}
13848