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::Attest(n) => {
125            n.leading_trivia = leading;
126            n.trailing_trivia = trailing;
127        }
128        Declaration::Window(n) => {
129            n.leading_trivia = leading;
130            n.trailing_trivia = trailing;
131        }
132        Declaration::Pix(n) => {
133            n.leading_trivia = leading;
134            n.trailing_trivia = trailing;
135        }
136        Declaration::Ledger(n) => {
137            n.leading_trivia = leading;
138            n.trailing_trivia = trailing;
139        }
140        Declaration::Psyche(n) => {
141            n.leading_trivia = leading;
142            n.trailing_trivia = trailing;
143        }
144        Declaration::Corpus(n) => {
145            n.leading_trivia = leading;
146            n.trailing_trivia = trailing;
147        }
148        Declaration::Dataspace(n) => {
149            n.leading_trivia = leading;
150            n.trailing_trivia = trailing;
151        }
152        Declaration::Ots(n) => {
153            n.leading_trivia = leading;
154            n.trailing_trivia = trailing;
155        }
156        Declaration::Mandate(n) => {
157            n.leading_trivia = leading;
158            n.trailing_trivia = trailing;
159        }
160        Declaration::Compute(n) => {
161            n.leading_trivia = leading;
162            n.trailing_trivia = trailing;
163        }
164        Declaration::Daemon(n) => {
165            n.leading_trivia = leading;
166            n.trailing_trivia = trailing;
167        }
168        Declaration::Extension(n) => {
169            n.leading_trivia = leading;
170            n.trailing_trivia = trailing;
171        }
172        Declaration::AxonStore(n) => {
173            n.leading_trivia = leading;
174            n.trailing_trivia = trailing;
175        }
176        Declaration::AxonEndpoint(n) => {
177            n.leading_trivia = leading;
178            n.trailing_trivia = trailing;
179        }
180        Declaration::Resource(n) => {
181            n.leading_trivia = leading;
182            n.trailing_trivia = trailing;
183        }
184        Declaration::Fabric(n) => {
185            n.leading_trivia = leading;
186            n.trailing_trivia = trailing;
187        }
188        Declaration::Manifest(n) => {
189            n.leading_trivia = leading;
190            n.trailing_trivia = trailing;
191        }
192        Declaration::Observe(n) => {
193            n.leading_trivia = leading;
194            n.trailing_trivia = trailing;
195        }
196        Declaration::Reconcile(n) => {
197            n.leading_trivia = leading;
198            n.trailing_trivia = trailing;
199        }
200        Declaration::Lease(n) => {
201            n.leading_trivia = leading;
202            n.trailing_trivia = trailing;
203        }
204        Declaration::Ensemble(n) => {
205            n.leading_trivia = leading;
206            n.trailing_trivia = trailing;
207        }
208        Declaration::Session(n) => {
209            n.leading_trivia = leading;
210            n.trailing_trivia = trailing;
211        }
212        Declaration::Topology(n) => {
213            n.leading_trivia = leading;
214            n.trailing_trivia = trailing;
215        }
216        Declaration::Immune(n) => {
217            n.leading_trivia = leading;
218            n.trailing_trivia = trailing;
219        }
220        Declaration::Reflex(n) => {
221            n.leading_trivia = leading;
222            n.trailing_trivia = trailing;
223        }
224        Declaration::Heal(n) => {
225            n.leading_trivia = leading;
226            n.trailing_trivia = trailing;
227        }
228        Declaration::Component(n) => {
229            n.leading_trivia = leading;
230            n.trailing_trivia = trailing;
231        }
232        Declaration::View(n) => {
233            n.leading_trivia = leading;
234            n.trailing_trivia = trailing;
235        }
236        Declaration::Channel(n) => {
237            n.leading_trivia = leading;
238            n.trailing_trivia = trailing;
239        }
240        Declaration::Socket(n) => {
241            n.leading_trivia = leading;
242            n.trailing_trivia = trailing;
243        }
244        Declaration::Upstream(n) => {
245            n.leading_trivia = leading;
246            n.trailing_trivia = trailing;
247        }
248        Declaration::Voice(n) => {
249            n.leading_trivia = leading;
250            n.trailing_trivia = trailing;
251        }
252        Declaration::Cors(n) => {
253            n.leading_trivia = leading;
254            n.trailing_trivia = trailing;
255        }
256        Declaration::Credential(n) => {
257            n.leading_trivia = leading;
258            n.trailing_trivia = trailing;
259        }
260        Declaration::Cache(n) => {
261            n.leading_trivia = leading;
262            n.trailing_trivia = trailing;
263        }
264        Declaration::Savant(n) => {
265            n.leading_trivia = leading;
266            n.trailing_trivia = trailing;
267        }
268        Declaration::Synth(n) => {
269            n.leading_trivia = leading;
270            n.trailing_trivia = trailing;
271        }
272        Declaration::Scope(n) => {
273            n.leading_trivia = leading;
274            n.trailing_trivia = trailing;
275        }
276        Declaration::Observable(n) => {
277            n.leading_trivia = leading;
278            n.trailing_trivia = trailing;
279        }
280        Declaration::Witness(n) => {
281            n.leading_trivia = leading;
282            n.trailing_trivia = trailing;
283        }
284        Declaration::Document(n) => {
285            n.leading_trivia = leading;
286            n.trailing_trivia = trailing;
287        }
288        Declaration::Deliver(n) => {
289            n.leading_trivia = leading;
290            n.trailing_trivia = trailing;
291        }
292        Declaration::Notify(n) => {
293            n.leading_trivia = leading;
294            n.trailing_trivia = trailing;
295        }
296        Declaration::Generic(n) => {
297            n.leading_trivia = leading;
298            n.trailing_trivia = trailing;
299        }
300    }
301}
302
303// ── Public error type ────────────────────────────────────────────────────────
304
305/// v1.20.0 — Source-context constants. D4 ratified 2026-05-10:
306/// 2 lines before + 2 lines after the error line. Mirror of the
307/// Python-side `_SOURCE_CONTEXT_LINES_BEFORE` / `_AFTER` so the
308/// rustc-style block has identical shape across stacks.
309pub const SOURCE_CONTEXT_LINES_BEFORE: usize = 2;
310pub const SOURCE_CONTEXT_LINES_AFTER: usize = 2;
311
312/// v1.20.0 — Rustc-style source-context block for a parse error.
313///
314/// Holds a reference to the source text plus the line/column the
315/// error points at. Rendering is lazy — call ``render()`` to format
316/// the block (line numbers + caret + 2 lines before + 2 after).
317///
318/// Pure and deterministic: no ANSI colors, no terminal-width
319/// detection. Output shape is byte-identical to the Python
320/// `SourceSnippet.render()` on the same input — that's the cross-
321/// stack drift gate (28.i).
322#[derive(Debug, Clone)]
323pub struct SourceSnippet {
324    pub source: String,
325    pub line: u32,
326    pub column: u32,
327    pub filename: String,
328    pub context_before: usize,
329    pub context_after: usize,
330}
331
332impl SourceSnippet {
333    /// Construct with the default 2/2 context window.
334    pub fn new(source: String, line: u32, column: u32, filename: String) -> Self {
335        Self {
336            source,
337            line,
338            column,
339            filename,
340            context_before: SOURCE_CONTEXT_LINES_BEFORE,
341            context_after: SOURCE_CONTEXT_LINES_AFTER,
342        }
343    }
344
345    /// Format the snippet as a multi-line rustc-style block.
346    ///
347    /// Empty source → empty string. Out-of-range line → empty
348    /// string. Caret column is clamped to `[1, line_len + 1]`.
349    /// Output shape matches Python `SourceSnippet.render` byte-
350    /// identically per D7.
351    #[must_use]
352    pub fn render(&self) -> String {
353        if self.source.is_empty() || self.line < 1 {
354            return String::new();
355        }
356        let raw: Vec<&str> = self.source.split('\n').collect();
357        // Match Python's str.splitlines() trailing-newline shape:
358        // strip an empty trailing entry produced by a final '\n'.
359        let lines: Vec<&str> = if raw.last() == Some(&"") {
360            raw[..raw.len() - 1].to_vec()
361        } else {
362            raw
363        };
364        if lines.is_empty() || self.line as usize > lines.len() {
365            return String::new();
366        }
367
368        let line_idx = self.line as usize;
369        let start = line_idx.saturating_sub(self.context_before).max(1);
370        let end = (line_idx + self.context_after).min(lines.len());
371
372        let gutter = end.to_string().len();
373        let empty_gutter = " ".repeat(gutter);
374
375        let mut out: Vec<String> = Vec::with_capacity(end - start + 4);
376        out.push(format!(
377            "{empty_gutter} --> {}:{}:{}",
378            self.filename, self.line, self.column
379        ));
380        out.push(format!("{empty_gutter} |"));
381        for n in start..=end {
382            let line_text = lines[n - 1];
383            out.push(format!("{n:>gutter$} | {line_text}", gutter = gutter));
384            if n == line_idx {
385                let line_len = line_text.chars().count();
386                let col = (self.column as usize).clamp(1, line_len + 1);
387                out.push(format!(
388                    "{empty_gutter} | {pad}^",
389                    pad = " ".repeat(col - 1)
390                ));
391            }
392        }
393        out.join("\n")
394    }
395}
396
397#[derive(Debug, Clone, Default)]
398pub struct ParseError {
399    pub message: String,
400    pub line: u32,
401    pub column: u32,
402    /// v1.20.0 — Optional rustc-style source-context block.
403    /// `None` preserves the legacy single-line shape; populated by
404    /// `Parser::with_source` callers (and by `parse_with_recovery`
405    /// / `parse` when a source has been attached to the parser).
406    /// Existing struct-literal call sites use the `..Default::default()`
407    /// idiom (default = None) to stay terse.
408    pub source_snippet: Option<SourceSnippet>,
409}
410
411impl ParseError {
412    /// v1.20.0 — Attach a `SourceSnippet` derived from raw source
413    /// text and filename. Returns `self` so the call can be chained
414    /// at the construction site. No-op when `line == 0`. Idempotent.
415    #[must_use]
416    pub fn attach_source(mut self, source: &str, filename: &str) -> Self {
417        if self.line >= 1 {
418            self.source_snippet = Some(SourceSnippet::new(
419                source.to_string(),
420                self.line,
421                self.column,
422                filename.to_string(),
423            ));
424        }
425        self
426    }
427}
428
429impl std::fmt::Display for ParseError {
430    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
431        write!(f, "[line {}:{}] {}", self.line, self.column, self.message)?;
432        if let Some(snippet) = &self.source_snippet {
433            let block = snippet.render();
434            if !block.is_empty() {
435                write!(f, "\n{block}")?;
436            }
437        }
438        Ok(())
439    }
440}
441
442impl std::error::Error for ParseError {}
443
444// ── v1.20.0 — Public recovery result ──────────────────────────────────────
445//
446// Mirror of Python's `axon.compiler.parser.ParseResult` (v1.20.0).
447// The rationale, sync semantics, and test contract are documented in
448// `the design plan`. The Rust frontend
449// must produce structurally identical error lists to the Python parser
450// when handed the same source — that is the cross-stack drift gate
451// (D7 ratified 2026-05-10: byte-identical error lists).
452//
453// `program` holds whatever declarations the parser was able to parse
454// successfully. `errors` holds every recovered error in source order.
455// A clean parse returns `errors.is_empty()`; the existing fail-fast
456// `parse()` API is preserved verbatim per D9.
457
458/// Outcome of `Parser::parse_with_recovery` — partial program plus the
459/// list of every error the parser recovered from. See module docs for
460/// the panic-mode + sync-point recovery semantics.
461#[derive(Debug)]
462pub struct ParseResult {
463    pub program: Program,
464    pub errors: Vec<ParseError>,
465}
466
467impl ParseResult {
468    /// True iff at least one parse error was recovered. Callers that
469    /// want to short-circuit on failure should check this rather than
470    /// relying on `program.declarations.is_empty()` (the parser may
471    /// have salvaged some declarations even with errors present).
472    #[inline]
473    #[must_use]
474    pub fn has_errors(&self) -> bool {
475        !self.errors.is_empty()
476    }
477
478    /// Inverse of `has_errors`. Convenience for the "happy path" check
479    /// in tests + adopter integrations.
480    #[inline]
481    #[must_use]
482    pub fn is_clean(&self) -> bool {
483        self.errors.is_empty()
484    }
485}
486
487/// v1.20.0 — Top-level declaration keywords used as resync points
488/// during error recovery (D2 ratified 2026-05-10). Mirrors the
489/// `_TOP_LEVEL_DECLARATION_KEYWORDS` frozenset on the Python side.
490///
491/// Distinct from `tokens::is_declaration_keyword` because that helper
492/// is used by the structural declaration counter and intentionally
493/// excludes some grammar-only tokens (Know/Believe/Speculate/Doubt,
494/// Ingest, Ots) that DO begin a top-level declaration in
495/// `parse_declaration` and therefore must be valid sync points.
496///
497/// Adding a new top-level dispatch arm in `parse_declaration` MUST
498/// add the corresponding token here so the recovery walker can
499/// re-sync correctly.
500#[inline]
501const fn is_top_level_decl_kw_for_recovery(tt: &TokenType) -> bool {
502    matches!(
503        tt,
504        TokenType::Import
505            | TokenType::Persona
506            | TokenType::Context
507            | TokenType::Anchor
508            | TokenType::Memory
509            | TokenType::Tool
510            | TokenType::Type
511            | TokenType::Flow
512            | TokenType::Intent
513            | TokenType::Run
514            | TokenType::Let
515            | TokenType::Know
516            | TokenType::Believe
517            | TokenType::Speculate
518            | TokenType::Doubt
519            | TokenType::Lambda
520            | TokenType::Agent
521            | TokenType::Shield
522            | TokenType::Pix
523            | TokenType::Ledger
524            | TokenType::Psyche
525            | TokenType::Corpus
526            | TokenType::Dataspace
527            | TokenType::Ots
528            | TokenType::Mandate
529            | TokenType::Compute
530            | TokenType::Daemon
531            // v2.42.0 — the autonomous research primitive + synth policy.
532            | TokenType::Savant
533            | TokenType::Synth
534            // v2.43.0 — the authorization-scope policy declaration.
535            | TokenType::Scope
536            | TokenType::AxonStore
537            | TokenType::AxonEndpoint
538            | TokenType::Resource
539            | TokenType::Fabric
540            | TokenType::Manifest
541            | TokenType::Observe
542            | TokenType::Reconcile
543            | TokenType::Lease
544            | TokenType::Ensemble
545            | TokenType::Session
546            | TokenType::Topology
547            | TokenType::Immune
548            | TokenType::Reflex
549            | TokenType::Heal
550            | TokenType::Component
551            | TokenType::View
552            | TokenType::Channel
553            | TokenType::Ingest
554            | TokenType::Persist
555            | TokenType::Retrieve
556            | TokenType::Mutate
557            | TokenType::Purge
558            | TokenType::Transact
559            | TokenType::Mcp
560    )
561}
562
563// ── v1.21.0 — axonendpoint transport + keepalive closed enums ────────────
564//
565// D2 ratified 2026-05-10: `transport` is a closed enum
566// {json, sse, ndjson}. D6 ratified: `keepalive` is a closed enum
567// {5s, 15s, 30s, 60s}. Both mirror the Python frontend's
568// `_AXONENDPOINT_TRANSPORT_VALUES` / `_AXONENDPOINT_KEEPALIVE_VALUES`
569// frozensets in `axon/compiler/parser.py`. Cross-stack drift gate
570// (30.b fixture) asserts byte-identical parse for every entry.
571
572/// Adopter-facing acceptable values for `transport:` field.
573/// Used by both the parser (validation + smart-suggest) and the
574/// type-checker (30.c) so adopter tooling sees one canonical list.
575pub const AXONENDPOINT_TRANSPORT_VALUES: &[&str] = &["json", "sse", "ndjson"];
576
577/// v1.28.0 — Closed-catalog SSE wire-format
578/// dialects. Selected via the parametrized grammar
579/// `transport: sse(<dialect>)`; bare `transport: sse` resolves to
580/// the Q1 default per the flow's algebraic-effect predicate
581/// (openai for tool-streaming flows; axon for type-annotation-only).
582///
583/// Vertical-grounded scope (Q3 revised 2026-05-14): five dialects
584/// cover ~99% of LLM-streaming adopter expectations.
585///   - `axon`      — current W3C named events
586///                   (event: axon.token / event: axon.complete).
587///                   D6 backwards-compat baseline; indefinitely
588///                   supported as a first-class option.
589///   - `openai`    — `data: {"choices":[{"delta":{...}}]}` frames
590///                   terminated by `data: [DONE]`. OpenAI Chat
591///                   Completions streaming wire verbatim.
592///   - `kimi`      — Moonshot Kimi (kimi.moonshot.cn) — uses the
593///                   OpenAI-compatible Chat Completions wire format
594///                   verbatim (same chunk shape, same `data: [DONE]`
595///                   sentinel). First-class entry so adopters
596///                   declare intent explicitly; under the hood the
597///                   wire is identical to `openai`.
598///   - `glm`       — Zhipu ChatGLM (open.bigmodel.cn) — same as
599///                   kimi, uses OpenAI-compat wire. First-class
600///                   entry for adopter clarity.
601///   - `anthropic` — `event: content_block_delta` frames terminated
602///                   by `event: message_stop`. Adopter SDKs
603///                   targeting Anthropic Claude consume this shape
604///                   verbatim.
605///
606/// Why kimi + glm as first-class entries (Q3 revision rationale):
607/// The project's primary adopter pipelines through Kimi K2.x +
608/// Zhipu GLM-4.x. While the wire IS byte-identical to OpenAI's
609/// Chat Completions streaming, declaring `transport: sse(kimi)` /
610/// `transport: sse(glm)` lets the audit trail + observability
611/// surfaces correlate adopter intent against the underlying
612/// provider — without the adopter having to know that "kimi
613/// happens to be OpenAI-compat on the wire today". The runtime
614/// dispatches kimi + glm to the same `OpenAIDialectAdapter` so
615/// the wire shape stays canonical-OpenAI-bytes.
616///
617/// Open-set adapter pluggability (downstream crates registering
618/// custom dialects) remains explicitly out of scope per the
619/// Axon-for-Axon discipline.
620pub const AXONENDPOINT_TRANSPORT_DIALECTS: &[&str] =
621    &["axon", "openai", "kimi", "glm", "anthropic"];
622
623/// Adopter-facing acceptable values for `keepalive:` field.
624pub const AXONENDPOINT_KEEPALIVE_VALUES: &[&str] = &["5s", "15s", "30s", "60s"];
625
626/// v1.23.0 D3 — Closed method enum for `method:` field. Adopter-
627/// declarable methods only; HEAD/OPTIONS/CONNECT/TRACE are
628/// runtime-managed (CORS preflight, etc.) and never declared from
629/// source. Closed enum refuses interpretation drift; smart-suggest
630/// catches near-misses at parse time.
631///
632/// v2.62.0 — `QUERY` (RFC 10008, Proposed Standard, June 2026): the safe +
633/// idempotent + cacheable method that CARRIES A REQUEST BODY — the first new HTTP
634/// method in two decades. It carries a LAW, not just a route: `axon-T927` refuses
635/// at compile time a QUERY endpoint whose flow performs a declared write (the
636/// RFC's normative "safe and idempotent" MUST, made a proof).
637///
638/// Must stay in lockstep with `type_checker::VALID_ENDPOINT_METHODS`.
639pub const AXONENDPOINT_METHOD_VALUES: &[&str] =
640    &["GET", "POST", "PUT", "DELETE", "PATCH", "QUERY"];
641
642/// v1.31.0 (D2) — Closed catalog for the `axonendpoint backend:`
643/// declaration. The set is `CANONICAL_PROVIDERS ∪ {auto, stub}`:
644///
645///   - the seven canonical LLM providers — `anthropic`, `gemini`,
646///     `glm`, `kimi`, `ollama`, `openai`, `openrouter` — a concrete,
647/// declared backend that rung 2 of the v1.31.0 D1 resolution
648///     ladder fires immediately;
649///   - `auto` — transparent: declaring it is equivalent to omitting
650///     `backend:` entirely (the route resolves down the ladder —
651///     server default → environment-available providers);
652///   - `stub` — the no-op backend, reachable ONLY by an explicit,
653///     written declaration (D5: a silent degradation to `stub` is
654///     forbidden; an explicit opt-in is not).
655///
656/// `axon-frontend` carries zero runtime deps and therefore cannot
657/// import `axon::backends::CANONICAL_PROVIDERS`; this list is a
658/// hand-maintained mirror. The axon-rs drift gate
659/// (`tests/backend_catalog_drift.rs`) asserts the two stay
660/// byte-identical — adding a provider in one place without the other
661/// fails CI.
662pub const AXONENDPOINT_BACKEND_VALUES: &[&str] = &[
663    "anthropic",
664    "auto",
665    "gemini",
666    "glm",
667    "kimi",
668    "ollama",
669    "openai",
670    "openrouter",
671    "stub",
672];
673
674#[inline]
675fn axonendpoint_is_valid_transport(s: &str) -> bool {
676    AXONENDPOINT_TRANSPORT_VALUES.iter().any(|&v| v == s)
677}
678
679#[inline]
680fn axonendpoint_is_valid_method(s: &str) -> bool {
681    AXONENDPOINT_METHOD_VALUES.iter().any(|&v| v == s)
682}
683
684#[inline]
685fn axonendpoint_is_valid_backend(s: &str) -> bool {
686    AXONENDPOINT_BACKEND_VALUES.iter().any(|&v| v == s)
687}
688
689#[inline]
690fn axonendpoint_is_valid_keepalive(s: &str) -> bool {
691    AXONENDPOINT_KEEPALIVE_VALUES.iter().any(|&v| v == s)
692}
693
694/// v1.32.0 (D2) — Closed type catalog for query parameters.
695///
696/// Query values arrive over HTTP as URL-encoded strings; the catalog
697/// is the set of types axon will validate / coerce them into for the
698/// Request Binding Contract. Hand-curated, intentionally small:
699///   - `Text` — the raw string (always succeeds)
700///   - `Int` — `i64` parseable
701///   - `Float` — `f64` parseable, finite
702///   - `Bool` — case-insensitive `{true, false, 1, 0, yes, no, on, off}`
703///   - `Uuid` — RFC 4122 textual form
704///
705/// Extending the catalog is a future axon-T?nn surface; v1.38.5 ships
706/// the 5 types covering ~95% of REST query patterns. Lists / dates /
707/// datetimes / enums are honest deferrals (see section 7 of the plan vivo).
708pub const AXONENDPOINT_QUERY_PARAM_TYPES: &[&str] =
709    &["Text", "Int", "Float", "Bool", "Uuid"];
710
711/// `true` iff `s` is one of the v1.32.0 (D2) query-param catalog
712/// entries — exact case-sensitive match (axon types are PascalCase).
713#[inline]
714pub(crate) fn axonendpoint_is_valid_query_param_type(s: &str) -> bool {
715    AXONENDPOINT_QUERY_PARAM_TYPES.iter().any(|&v| v == s)
716}
717
718/// v1.32.0 (D1) — Extract `{name}` placeholder names from an
719/// `axonendpoint` `path:` string, in left-to-right declaration order.
720///
721/// Recognized placeholder grammar (single-segment, no nested braces):
722/// `{NAME}` where `NAME` matches `[A-Za-z_][A-Za-z0-9_]*`. Anything
723/// inside braces that does NOT match the identifier shape is silently
724/// IGNORED — it's either an adopter typo (caught later by axum at
725/// route registration) or a literal brace in the URL pattern.
726///
727/// Returns `Err(duplicate_name)` when the same `{name}` appears more
728/// than once in the path — HTTP route patterns reject duplicates
729/// structurally (`axum` would panic at registration), so surfacing
730/// the error at parse time is the right place.
731///
732/// Pure + total: never panics; deterministic over its single string
733/// argument. Hand-rolled scanner (no regex dep at parser layer).
734///
735/// # Examples
736///
737/// - `"/api/users"` → `Ok(vec![])`
738/// - `"/api/users/{id}"` → `Ok(vec!["id"])`
739/// - `"/api/tenants/{tenant_id}/secrets/{secret_name}"`
740///   → `Ok(vec!["tenant_id", "secret_name"])`
741/// - `"/api/users/{id}/posts/{id}"` → `Err("id")` (duplicate)
742/// - `"/api/{not valid}"` → `Ok(vec![])` (malformed brace content
743///   silently ignored; axum surfaces the error at registration)
744pub(crate) fn extract_path_param_names(path: &str) -> Result<Vec<String>, String> {
745    let mut out: Vec<String> = Vec::new();
746    let bytes = path.as_bytes();
747    let mut i = 0;
748    while i < bytes.len() {
749        if bytes[i] != b'{' {
750            i += 1;
751            continue;
752        }
753        // Find the matching close brace; if none, the open brace is
754        // a literal — leave it alone.
755        let start = i + 1;
756        let mut end = start;
757        while end < bytes.len() && bytes[end] != b'}' {
758            end += 1;
759        }
760        if end == bytes.len() {
761            // Unterminated — give up; downstream parser/runtime
762            // surface the malformed path elsewhere.
763            break;
764        }
765        let raw = &path[start..end];
766        // Validate identifier shape: [A-Za-z_][A-Za-z0-9_]*
767        let valid = !raw.is_empty()
768            && raw.bytes().enumerate().all(|(idx, b)| {
769                if idx == 0 {
770                    b.is_ascii_alphabetic() || b == b'_'
771                } else {
772                    b.is_ascii_alphanumeric() || b == b'_'
773                }
774            });
775        if valid {
776            let name = raw.to_string();
777            if out.iter().any(|existing| existing == &name) {
778                return Err(name);
779            }
780            out.push(name);
781        }
782        i = end + 1;
783    }
784    Ok(out)
785}
786
787/// v1.23.0 (D8) — Closed capability-slug grammar. Validates a
788/// `requires:` slug per `^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$`.
789///
790/// Hand-rolled (no regex dep at parser layer) — each segment must
791/// match `[a-z][a-z0-9_]*` and segments are joined by single dots.
792/// Public so the runtime mirror (`axon::auth_scope`) reuses the same
793/// predicate without duplicating the rule.
794///
795/// Examples valid: `admin`, `legal.read`, `hipaa.phi.read`,
796/// `bank.officer.senior`, `a`, `a_b`, `a1`.
797/// Examples invalid: empty, `Admin` (uppercase), `1admin` (digit
798/// first), `bank-officer` (hyphen), `bank..a` (empty segment),
799/// `.admin`, `admin.`, `admin..` .
800pub fn is_valid_capability_slug(slug: &str) -> bool {
801    if slug.is_empty() {
802        return false;
803    }
804    for segment in slug.split('.') {
805        if !is_valid_slug_segment(segment) {
806            return false;
807        }
808    }
809    true
810}
811
812fn is_valid_slug_segment(seg: &str) -> bool {
813    let mut chars = seg.chars();
814    let first = match chars.next() {
815        Some(c) => c,
816        None => return false,
817    };
818    if !first.is_ascii_lowercase() {
819        return false;
820    }
821    chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
822}
823
824// ════════════════════════════════════════════════════════════════════
825// v1.32.0 (D1) — `extract_path_param_names` unit tests
826// ════════════════════════════════════════════════════════════════════
827
828// ════════════════════════════════════════════════════════════════════
829// v1.32.0 (D2) — `axonendpoint_is_valid_query_param_type` + the
830//  inline `query: { … }` parser, end-to-end through the lexer.
831// ════════════════════════════════════════════════════════════════════
832
833#[cfg(test)]
834mod query_param_catalog_tests {
835    use super::{axonendpoint_is_valid_query_param_type, AXONENDPOINT_QUERY_PARAM_TYPES};
836
837    #[test]
838    fn accepts_every_catalog_entry() {
839        for ty in AXONENDPOINT_QUERY_PARAM_TYPES {
840            assert!(
841                axonendpoint_is_valid_query_param_type(ty),
842                "catalog entry `{ty}` must validate"
843            );
844        }
845    }
846
847    #[test]
848    fn rejects_off_catalog_types() {
849        for off in &[
850            "Timestamp",    // not in v1.38.5 — list/dates deferred
851            "Date",
852            "DateTime",
853            "List<Text>", // multi-value query params deferred (section 7)
854            "Jsonb",        // store-only types not query-applicable
855            "Bytea",
856            "text",         // lowercase rejected (axon types are PascalCase)
857            "TEXT",
858            "Number",       // not in axon's type catalog at all
859            "",             // empty
860            " ",            // whitespace
861        ] {
862            assert!(
863                !axonendpoint_is_valid_query_param_type(off),
864                "off-catalog `{off}` must reject"
865            );
866        }
867    }
868
869    #[test]
870    fn catalog_size_matches_design() {
871        // The plan vivo D2 states a closed 5-type catalog. A future
872        // axon-T?nn surface may extend it; that requires updating BOTH
873        // the catalog AND the plan vivo section 7 honest-scope note.
874        assert_eq!(AXONENDPOINT_QUERY_PARAM_TYPES.len(), 5);
875    }
876}
877
878#[cfg(test)]
879mod query_param_parser_tests {
880    use crate::lexer::Lexer;
881    use crate::parser::Parser;
882
883    fn parse_endpoint_source(src: &str) -> Result<crate::ast::AxonEndpointDefinition, String> {
884        let tokens = Lexer::new(src, "test.axon")
885            .tokenize()
886            .map_err(|e| format!("lex: {}", e.message))?;
887        let mut parser = Parser::new(tokens);
888        let program = parser.parse().map_err(|e| format!("parse: {}", e.message))?;
889        program
890            .declarations
891            .into_iter()
892            .find_map(|d| match d {
893                crate::ast::Declaration::AxonEndpoint(e) => Some(e),
894                _ => None,
895            })
896            .ok_or_else(|| "no axonendpoint in program".to_string())
897    }
898
899    #[test]
900    fn endpoint_with_no_query_block_keeps_empty_vec() {
901        let src = r#"
902            axonendpoint write_secret {
903                method: POST
904                path: "/api/users"
905                body: SecretWriteRequest
906                execute: WriteSecret
907            }
908        "#;
909        let ep = parse_endpoint_source(src).expect("parses");
910        assert!(
911            ep.query_params.is_empty(),
912            "D5 — no `query:` block ⇒ empty query_params"
913        );
914    }
915
916    #[test]
917    fn single_query_param_required() {
918        let src = r#"
919            axonendpoint list_users {
920                method: GET
921                path: "/api/users"
922                query: { status: Text }
923                execute: ListUsers
924            }
925        "#;
926        let ep = parse_endpoint_source(src).expect("parses");
927        assert_eq!(ep.query_params.len(), 1);
928        assert_eq!(ep.query_params[0].name, "status");
929        assert_eq!(ep.query_params[0].type_expr.name, "Text");
930        assert!(!ep.query_params[0].type_expr.optional);
931    }
932
933    #[test]
934    fn optional_query_param_via_question_suffix() {
935        let src = r#"
936            axonendpoint list_users {
937                method: GET
938                path: "/api/users"
939                query: { limit: Int? }
940                execute: ListUsers
941            }
942        "#;
943        let ep = parse_endpoint_source(src).expect("parses");
944        assert_eq!(ep.query_params.len(), 1);
945        assert_eq!(ep.query_params[0].name, "limit");
946        assert_eq!(ep.query_params[0].type_expr.name, "Int");
947        assert!(
948            ep.query_params[0].type_expr.optional,
949            "`?` suffix sets optional"
950        );
951    }
952
953    #[test]
954    fn multiple_query_params_preserve_declaration_order() {
955        let src = r#"
956            axonendpoint search {
957                method: GET
958                path: "/api/search"
959                query: { q: Text, page: Int?, limit: Int?, exact: Bool? }
960                execute: Search
961            }
962        "#;
963        let ep = parse_endpoint_source(src).expect("parses");
964        let names: Vec<&str> = ep.query_params.iter().map(|f| f.name.as_str()).collect();
965        assert_eq!(names, vec!["q", "page", "limit", "exact"]);
966        let types: Vec<&str> = ep
967            .query_params
968            .iter()
969            .map(|f| f.type_expr.name.as_str())
970            .collect();
971        assert_eq!(types, vec!["Text", "Int", "Int", "Bool"]);
972        let optionals: Vec<bool> = ep
973            .query_params
974            .iter()
975            .map(|f| f.type_expr.optional)
976            .collect();
977        assert_eq!(optionals, vec![false, true, true, true]);
978    }
979
980    #[test]
981    fn duplicate_query_param_is_parse_error() {
982        let src = r#"
983            axonendpoint bad {
984                method: GET
985                path: "/api/x"
986                query: { name: Text, name: Int? }
987                execute: Bad
988            }
989        "#;
990        let err = parse_endpoint_source(src).expect_err("must fail");
991        assert!(
992            err.contains("duplicate query param 'name'"),
993            "error must name the duplicate. Got: {err}"
994        );
995    }
996
997    #[test]
998    fn off_catalog_type_with_smart_suggest_hint() {
999        // `Strng` is one edit away from `Text` (would suggest `Text`?
1000        // Actually edit distance to `Text` is 4; to `Int` is 5. Likely
1001        // no smart suggestion within distance 2. The error still names
1002        // the catalog explicitly.)
1003        let src = r#"
1004            axonendpoint bad {
1005                method: GET
1006                path: "/api/x"
1007                query: { value: Strng }
1008                execute: Bad
1009            }
1010        "#;
1011        let err = parse_endpoint_source(src).expect_err("must fail");
1012        assert!(
1013            err.contains("unsupported type 'Strng'"),
1014            "error must name the bad type. Got: {err}"
1015        );
1016        assert!(
1017            err.contains("Expected one of: Text | Int | Float | Bool | Uuid"),
1018            "error must list the closed catalog. Got: {err}"
1019        );
1020    }
1021
1022    #[test]
1023    fn close_typo_gets_did_you_mean_hint() {
1024        // `Txt` → edit distance 1 from `Text` → smart-suggest should
1025        // surface the hint.
1026        let src = r#"
1027            axonendpoint bad {
1028                method: GET
1029                path: "/api/x"
1030                query: { value: Txt }
1031                execute: Bad
1032            }
1033        "#;
1034        let err = parse_endpoint_source(src).expect_err("must fail");
1035        assert!(
1036            err.contains("Did you mean") && err.contains("`Text`"),
1037            "smart-suggest must hint `Text`. Got: {err}"
1038        );
1039    }
1040
1041    #[test]
1042    fn every_catalog_type_parses_cleanly() {
1043        // Round-trip smoke for all 5 catalog entries.
1044        for ty in &["Text", "Int", "Float", "Bool", "Uuid"] {
1045            let src = format!(
1046                r#"
1047                    axonendpoint x {{
1048                        method: GET
1049                        path: "/api/x"
1050                        query: {{ v: {ty} }}
1051                        execute: X
1052                    }}
1053                "#
1054            );
1055            let ep = parse_endpoint_source(&src)
1056                .unwrap_or_else(|e| panic!("`{ty}` should parse: {e}"));
1057            assert_eq!(ep.query_params[0].type_expr.name, *ty);
1058        }
1059    }
1060
1061    #[test]
1062    fn comma_optional_between_params() {
1063        // The plan vivo design accepts both comma-separated and
1064        // whitespace-separated query params (existing parser style is
1065        // forgiving). Whitespace-only:
1066        let src = r#"
1067            axonendpoint x {
1068                method: GET
1069                path: "/api/x"
1070                query: { a: Text b: Int? }
1071                execute: X
1072            }
1073        "#;
1074        let ep = parse_endpoint_source(src).expect("parses without commas");
1075        assert_eq!(ep.query_params.len(), 2);
1076    }
1077
1078    // ─── Robustness hardening (37.y.2 100% robust closure) ──────────
1079
1080    #[test]
1081    fn double_query_block_is_parse_error() {
1082        // An adopter who copy-pastes the `query:` block twice should
1083        // see a clear parse error, not a silent merge that produces
1084        // an unexpectedly-augmented endpoint with both blocks fused.
1085        let src = r#"
1086            axonendpoint x {
1087                method: GET
1088                path: "/api/x"
1089                query: { a: Text }
1090                query: { b: Int? }
1091                execute: X
1092            }
1093        "#;
1094        let err = parse_endpoint_source(src).expect_err("must fail");
1095        assert!(
1096            err.contains("declares `query: { … }` more than once"),
1097            "error must call out the duplicate block. Got: {err}"
1098        );
1099        assert!(
1100            err.contains("combine all params into a single block"),
1101            "error must hint the canonical fix. Got: {err}"
1102        );
1103    }
1104
1105    #[test]
1106    fn optional_generic_type_is_parse_error_with_canonical_hint() {
1107        // `Optional<Text>` is the wrong way to declare an optional
1108        // query param. The canonical syntax is `Text?` (the `?`
1109        // suffix). The error must surface this with a literal example.
1110        let src = r#"
1111            axonendpoint x {
1112                method: GET
1113                path: "/api/x"
1114                query: { value: Optional<Text> }
1115                execute: X
1116            }
1117        "#;
1118        let err = parse_endpoint_source(src).expect_err("must fail");
1119        assert!(
1120            err.contains("generic type `Optional<Text>`"),
1121            "error must name the generic type literally. Got: {err}"
1122        );
1123        assert!(
1124            err.contains("Use `Text?` (the `?` suffix)"),
1125            "error must hint the canonical `Text?` syntax. Got: {err}"
1126        );
1127    }
1128
1129    #[test]
1130    fn list_generic_type_is_parse_error_with_deferral_hint() {
1131        // Multi-value query params (`?tag=a&tag=b`) are honest-
1132        // deferred per the plan vivo section 7. Adopters who write
1133        // `List<Text>` should see a clear error explaining the
1134        // deferral, not a confusing "type `List` not in catalog".
1135        let src = r#"
1136            axonendpoint x {
1137                method: GET
1138                path: "/api/x"
1139                query: { tags: List<Text> }
1140                execute: X
1141            }
1142        "#;
1143        let err = parse_endpoint_source(src).expect_err("must fail");
1144        assert!(
1145            err.contains("generic type `List<Text>`"),
1146            "error must name the generic type. Got: {err}"
1147        );
1148        assert!(
1149            err.contains("Multi-value query params")
1150                && err.contains("honest-deferred"),
1151            "error must mention the multi-value deferral. Got: {err}"
1152        );
1153    }
1154
1155    #[test]
1156    fn other_generic_types_caught_generically() {
1157        // Generic types beyond `Optional` and `List` get the
1158        // generic-rejection message without a canonical-syntax hint
1159        // (the catalog list is the canonical guidance).
1160        let src = r#"
1161            axonendpoint x {
1162                method: GET
1163                path: "/api/x"
1164                query: { value: Stream<Int> }
1165                execute: X
1166            }
1167        "#;
1168        let err = parse_endpoint_source(src).expect_err("must fail");
1169        assert!(
1170            err.contains("generic type `Stream<Int>`"),
1171            "error must name the generic type. Got: {err}"
1172        );
1173        assert!(
1174            err.contains("Text | Int | Float | Bool | Uuid"),
1175            "error must list the closed catalog. Got: {err}"
1176        );
1177    }
1178
1179    #[test]
1180    fn uuid_optional_parses_cleanly() {
1181        // Hardening companion — `Uuid?` is in the catalog AND
1182        // optional. The two features compose without surprise.
1183        let src = r#"
1184            axonendpoint find {
1185                method: GET
1186                path: "/api/x"
1187                query: { after: Uuid? }
1188                execute: Find
1189            }
1190        "#;
1191        let ep = parse_endpoint_source(src).expect("parses");
1192        assert_eq!(ep.query_params.len(), 1);
1193        assert_eq!(ep.query_params[0].name, "after");
1194        assert_eq!(ep.query_params[0].type_expr.name, "Uuid");
1195        assert!(ep.query_params[0].type_expr.optional);
1196        assert_eq!(ep.query_params[0].type_expr.generic_param, "");
1197    }
1198
1199    #[test]
1200    fn empty_query_block_yields_empty_vec() {
1201        // `query: { }` is grammatically valid but semantically a
1202        // no-op (equivalent to omitting the block). Don't error;
1203        // just record an empty Vec.
1204        let src = r#"
1205            axonendpoint x {
1206                method: GET
1207                path: "/api/x"
1208                query: { }
1209                execute: X
1210            }
1211        "#;
1212        let ep = parse_endpoint_source(src).expect("empty block parses");
1213        assert!(ep.query_params.is_empty());
1214    }
1215
1216    #[test]
1217    fn kivi_secret_write_path_plus_query() {
1218        // Combined path-param + query-param test: an endpoint that
1219        // takes IDs in the URL AND optional filters in the query
1220        // string. This is the natural REST shape v1.32.0 serves.
1221        let src = r#"
1222            axonendpoint write_secret {
1223                method: POST
1224                path: "/api/tenants/{tenant_id}/secrets/{secret_name}"
1225                query: { dry_run: Bool?, overwrite: Bool? }
1226                body: SecretWriteRequest
1227                execute: WriteSecret
1228            }
1229        "#;
1230        let ep = parse_endpoint_source(src).expect("parses");
1231        // Path params populated (from 37.y.1):
1232        assert_eq!(ep.path_params, vec!["tenant_id", "secret_name"]);
1233        // Query params populated (from this step 37.y.2):
1234        assert_eq!(ep.query_params.len(), 2);
1235        assert_eq!(ep.query_params[0].name, "dry_run");
1236        assert_eq!(ep.query_params[0].type_expr.name, "Bool");
1237        assert!(ep.query_params[0].type_expr.optional);
1238        assert_eq!(ep.query_params[1].name, "overwrite");
1239        // Body still works:
1240        assert_eq!(ep.body_type, "SecretWriteRequest");
1241    }
1242}
1243
1244#[cfg(test)]
1245mod path_param_extraction_tests {
1246    use super::extract_path_param_names;
1247
1248    #[test]
1249    fn empty_path_no_placeholders() {
1250        assert_eq!(extract_path_param_names("/api/users"), Ok(vec![]));
1251        assert_eq!(extract_path_param_names("/"), Ok(vec![]));
1252        assert_eq!(extract_path_param_names(""), Ok(vec![]));
1253    }
1254
1255    #[test]
1256    fn single_placeholder() {
1257        assert_eq!(
1258            extract_path_param_names("/api/users/{id}"),
1259            Ok(vec!["id".to_string()])
1260        );
1261    }
1262
1263    #[test]
1264    fn multiple_placeholders_in_declaration_order() {
1265        assert_eq!(
1266            extract_path_param_names(
1267                "/api/tenants/{tenant_id}/secrets/{secret_name}"
1268            ),
1269            Ok(vec![
1270                "tenant_id".to_string(),
1271                "secret_name".to_string(),
1272            ])
1273        );
1274    }
1275
1276    #[test]
1277    fn kivi_chat_history_path_pattern() {
1278        // The exact pattern the kivi adopter reported (2026-05-20):
1279        // POST /api/tenants/{tenant_id}/secrets/{secret_name}
1280        // Both names extracted in source order.
1281        let names = extract_path_param_names(
1282            "/api/tenants/{tenant_id}/secrets/{secret_name}",
1283        );
1284        assert_eq!(
1285            names,
1286            Ok(vec![
1287                "tenant_id".to_string(),
1288                "secret_name".to_string(),
1289            ])
1290        );
1291    }
1292
1293    #[test]
1294    fn duplicate_placeholder_returns_err() {
1295        assert_eq!(
1296            extract_path_param_names("/api/users/{id}/posts/{id}"),
1297            Err("id".to_string())
1298        );
1299    }
1300
1301    #[test]
1302    fn underscore_and_numeric_in_name() {
1303        assert_eq!(
1304            extract_path_param_names("/api/{user_id}/items/{item_2}"),
1305            Ok(vec!["user_id".to_string(), "item_2".to_string()])
1306        );
1307    }
1308
1309    #[test]
1310    fn leading_underscore_accepted() {
1311        // Identifiers in HTTP paths often start with letters but the
1312        // grammar permits leading underscore (parity with Rust identifier
1313        // rules). The flow parameter name on the binding side has to
1314        // match exactly, so adopters with `_internal_id` in the path
1315        // can pair it with a same-named flow param.
1316        assert_eq!(
1317            extract_path_param_names("/api/{_internal}"),
1318            Ok(vec!["_internal".to_string()])
1319        );
1320    }
1321
1322    #[test]
1323    fn malformed_placeholder_silently_ignored() {
1324        // Content inside `{...}` that does not match the identifier
1325        // grammar is skipped at this layer. axum surfaces the route
1326        // registration failure if the literal text is invalid.
1327        assert_eq!(
1328            extract_path_param_names("/api/{not valid}"),
1329            Ok(vec![])
1330        );
1331        // Empty braces — same: skip silently.
1332        assert_eq!(extract_path_param_names("/api/{}"), Ok(vec![]));
1333        // Mixed: malformed brace skipped, valid placeholder kept.
1334        assert_eq!(
1335            extract_path_param_names("/api/{tenant id}/users/{id}"),
1336            Ok(vec!["id".to_string()])
1337        );
1338    }
1339
1340    #[test]
1341    fn unterminated_brace_returns_clean() {
1342        // Open brace with no close brace — give up without panicking.
1343        // (axum surfaces the malformed-route error at registration.)
1344        assert_eq!(extract_path_param_names("/api/{id"), Ok(vec![]));
1345    }
1346
1347    #[test]
1348    fn placeholders_at_path_boundaries() {
1349        // Placeholder as the very first segment AND the very last
1350        // segment — both should be extracted.
1351        assert_eq!(
1352            extract_path_param_names("{prefix}/api/users/{id}"),
1353            Ok(vec!["prefix".to_string(), "id".to_string()])
1354        );
1355        assert_eq!(
1356            extract_path_param_names("/api/{id}"),
1357            Ok(vec!["id".to_string()])
1358        );
1359    }
1360
1361    #[test]
1362    fn deduplication_detects_non_adjacent_duplicates() {
1363        // The duplicate-detection sweep is global, not just adjacent.
1364        assert_eq!(
1365            extract_path_param_names(
1366                "/api/orgs/{org_id}/teams/{team_id}/repos/{org_id}"
1367            ),
1368            Err("org_id".to_string())
1369        );
1370    }
1371
1372    #[test]
1373    fn never_panics_on_arbitrary_input() {
1374        // Light fuzz: a handful of weird inputs return cleanly.
1375        for input in &[
1376            "{",
1377            "}",
1378            "{}",
1379            "{{}}",
1380            "{{{",
1381            "/api/{}/{id}",
1382            "////",
1383            "\u{1F4A1}",        // emoji (lightbulb)
1384            "\u{0000}",         // null byte
1385        ] {
1386            let _ = extract_path_param_names(input); // must not panic
1387        }
1388    }
1389}
1390
1391#[cfg(test)]
1392mod capability_slug_tests {
1393    use super::is_valid_capability_slug;
1394
1395    #[test]
1396    fn accepts_canonical_examples() {
1397        assert!(is_valid_capability_slug("admin"));
1398        assert!(is_valid_capability_slug("legal.read"));
1399        assert!(is_valid_capability_slug("hipaa.phi.read"));
1400        assert!(is_valid_capability_slug("bank.officer.senior"));
1401        assert!(is_valid_capability_slug("a"));
1402        assert!(is_valid_capability_slug("a_b"));
1403        assert!(is_valid_capability_slug("a1"));
1404        assert!(is_valid_capability_slug("a.b1_c"));
1405    }
1406
1407    #[test]
1408    fn rejects_empty_string() {
1409        assert!(!is_valid_capability_slug(""));
1410    }
1411
1412    #[test]
1413    fn rejects_uppercase() {
1414        assert!(!is_valid_capability_slug("Admin"));
1415        assert!(!is_valid_capability_slug("admin.READ"));
1416    }
1417
1418    #[test]
1419    fn rejects_digit_first() {
1420        assert!(!is_valid_capability_slug("1admin"));
1421        assert!(!is_valid_capability_slug("admin.1read"));
1422    }
1423
1424    #[test]
1425    fn rejects_hyphen() {
1426        assert!(!is_valid_capability_slug("bank-officer"));
1427    }
1428
1429    #[test]
1430    fn rejects_empty_segments() {
1431        assert!(!is_valid_capability_slug("bank..a"));
1432        assert!(!is_valid_capability_slug(".admin"));
1433        assert!(!is_valid_capability_slug("admin."));
1434    }
1435
1436    #[test]
1437    fn rejects_special_chars() {
1438        assert!(!is_valid_capability_slug("admin@read"));
1439        assert!(!is_valid_capability_slug("admin/read"));
1440        assert!(!is_valid_capability_slug("admin read"));
1441    }
1442}
1443
1444// ── Parser ───────────────────────────────────────────────────────────────────
1445
1446pub struct Parser {
1447    tokens: Vec<Token>,
1448    pos: usize,
1449    /// v2.83.0 — declarations lifted out of a FLOW BODY to program level.
1450    ///
1451    /// README nests an epistemic block inside a flow to scope the helper
1452    /// flows it calls:
1453    ///
1454    /// ```text
1455    /// flow MarketIntelligence(sector: String) -> Report {
1456    ///     know { flow GatherData(sector: String) -> DataSet { … } }
1457    ///     par { … }
1458    /// }
1459    /// ```
1460    ///
1461    /// A top-level `know { … }` already HOISTS its children into the
1462    /// program-level IR collections, stamping `epistemic_mode` on each
1463    /// (`ir_generator`, v2.53.0/v2.60.0/v2.66.0). Hoisting the nested one to a
1464    /// top-level `Declaration::Epistemic` therefore makes it byte-identical
1465    /// to the form that already works — zero new handling in the checker, the
1466    /// IR generator, or the runtime. The alternative (a new FlowStep variant
1467    /// carrying declarations) would fork every one of those.
1468    hoisted: Vec<Declaration>,
1469    /// v1.5.2 — leading trivia parallel array, indexed by the
1470    /// effective-token position. `leading_trivia[i]` is the comment
1471    /// trivia that appeared between the previous effective token (or
1472    /// file start) and `tokens[i]`.
1473    leading_trivia: Vec<Vec<Trivia>>,
1474    /// v1.5.2 — trailing trivia parallel array. `trailing_trivia[i]`
1475    /// is the comment trivia on the same line as `tokens[i]`, before
1476    /// the next effective token. Populated by the constructor.
1477    trailing_trivia: Vec<Vec<Trivia>>,
1478    /// v1.12.0 — side-channel for tagging let value_kind. Set by
1479    /// `parse_let_atom` / `parse_let_value_expr` as they descend; read
1480    /// at the end of `parse_let` and stored on the LetStatement.
1481    last_let_value_kind: String,
1482    /// v1.14.0 — loop nesting depth for break/continue scope check.
1483    /// Incremented at the start of `parse_for_in`, decremented after.
1484    /// `parse_break`/`parse_continue` raise ParseError when this is
1485    /// zero (the keyword has no meaning outside a loop body).
1486    loop_depth: u32,
1487    /// v1.20.0 — Optional source text + filename for the rustc-
1488    /// style source-context block on `ParseError`. Set via the
1489    /// fluent `Parser::with_source` builder; default `None` keeps
1490    /// existing callers (`Parser::new(tokens).parse()`) emitting
1491    /// the legacy single-line shape.
1492    source: Option<String>,
1493    filename: String,
1494}
1495
1496impl Parser {
1497    pub fn new(raw_tokens: Vec<Token>) -> Self {
1498        // ── v1.5.2 — split the raw token stream into:
1499        //   - effective tokens the grammar consumes (cursor advances
1500        //     over these as before),
1501        //   - parallel `leading_trivia` / `trailing_trivia` arrays
1502        //     indexed by effective-token position.
1503        // Comments on a fresh line attach as leading trivia of the
1504        // next effective token; comments on the same line as an
1505        // effective token attach as trailing trivia of that token.
1506        // Roslyn/Swift convention.
1507        let mut effective: Vec<Token> = Vec::with_capacity(raw_tokens.len());
1508        let mut leading: Vec<Vec<Trivia>> = Vec::with_capacity(raw_tokens.len());
1509        let mut trailing: Vec<Vec<Trivia>> = Vec::with_capacity(raw_tokens.len());
1510
1511        let mut pending_leading: Vec<Trivia> = Vec::new();
1512        let mut last_effective_line: i64 = -1;
1513        for tok in raw_tokens {
1514            if is_comment_token(&tok.ttype) {
1515                let kind = token_to_trivia_kind(&tok.ttype)
1516                    .expect("comment token must map to a trivia kind");
1517                let triv = Trivia {
1518                    kind,
1519                    text: tok.value,
1520                    line: tok.line,
1521                    column: tok.column,
1522                };
1523                if !effective.is_empty() && (tok.line as i64) == last_effective_line {
1524                    trailing.last_mut().unwrap().push(triv);
1525                } else {
1526                    pending_leading.push(triv);
1527                }
1528            } else {
1529                last_effective_line = tok.line as i64;
1530                effective.push(tok);
1531                leading.push(std::mem::take(&mut pending_leading));
1532                trailing.push(Vec::new());
1533            }
1534        }
1535
1536        Parser {
1537            hoisted: Vec::new(),
1538            tokens: effective,
1539            pos: 0,
1540            leading_trivia: leading,
1541            trailing_trivia: trailing,
1542            last_let_value_kind: "literal".to_string(),
1543            loop_depth: 0,
1544            source: None,
1545            filename: "<source>".to_string(),
1546        }
1547    }
1548
1549    /// v1.20.0 — Fluent attach of source text + filename for
1550    /// rustc-style source-context blocks on emitted `ParseError`s.
1551    /// Returns `self` so it chains with `.parse_with_recovery()`:
1552    ///
1553    /// ```ignore
1554    /// let result = Parser::new(tokens)
1555    ///     .with_source(src, "foo.axon")
1556    ///     .parse_with_recovery();
1557    /// ```
1558    ///
1559    /// No-op of any other behaviour — pure metadata attach.
1560    #[must_use]
1561    pub fn with_source(mut self, source: &str, filename: &str) -> Self {
1562        self.source = Some(source.to_string());
1563        self.filename = filename.to_string();
1564        self
1565    }
1566
1567    // ── public API ───────────────────────────────────────────────
1568
1569    pub fn parse(&mut self) -> Result<Program, ParseError> {
1570        let mut program = Program {
1571            declarations: Vec::new(),
1572            declaration_trivia: Vec::new(),
1573            loc: Loc { line: 1, column: 1 },
1574        };
1575        while !self.check(TokenType::Eof) {
1576            // Capture trivia around the declaration. `start_pos` is
1577            // the effective-token position of the declaration's first
1578            // token; that position carries the leading trivia. After
1579            // parsing, `pos - 1` is the last token consumed; that
1580            // position carries the trailing trivia.
1581            let start_pos = self.pos;
1582            let mut decl = match self.parse_declaration() {
1583                Ok(d) => d,
1584                Err(e) => return Err(self.attach_source_to_error(e)),
1585            };
1586            let end_pos = self.pos.saturating_sub(1);
1587            let leading = self
1588                .leading_trivia
1589                .get(start_pos)
1590                .cloned()
1591                .unwrap_or_default();
1592            let trailing = self
1593                .trailing_trivia
1594                .get(end_pos)
1595                .cloned()
1596                .unwrap_or_default();
1597            // v1.5.2 — also copy trivia into the per-struct fields on
1598            // the declaration so consumers can read `flow.leading_trivia`
1599            // directly without going through `program.declaration_trivia[i]`.
1600            // The side-channel is preserved for backward compat with
1601            // 14.a callers and as a flat enumeration source.
1602            attach_trivia_to_decl(&mut decl, leading.clone(), trailing.clone());
1603            program.declarations.push(decl);
1604            program
1605                .declaration_trivia
1606                .push(DeclarationTrivia { leading, trailing });
1607            // v2.83.0 — drain anything a flow body hoisted to program
1608            // level. Appended AFTER the enclosing declaration so source order
1609            // still reads top-to-bottom in `axon desugar`.
1610            for hoisted in std::mem::take(&mut self.hoisted) {
1611                program.declarations.push(hoisted);
1612                program.declaration_trivia.push(DeclarationTrivia {
1613                    leading: Vec::new(),
1614                    trailing: Vec::new(),
1615                });
1616            }
1617        }
1618        // v2.37.0 — expand `voice` declarations FIRST (they may emit
1619        // `from Preset@vN` upstream legs), then v2.37.0 preset references,
1620        // BEFORE type-check — so the v2.37.0 laws and the IR see the expanded
1621        // program (and `axon desugar` prints exactly this lowering).
1622        // Unknown presets stay unexpanded — the checker reports them with
1623        // the catalog list (accumulating diagnostics beat a parse abort).
1624        crate::voice_desugar::expand(&mut program);
1625        crate::upstream_presets::expand(&mut program);
1626        Ok(program)
1627    }
1628
1629    // ── v1.20.0 — recovery-mode parse ─────────────────────────
1630    //
1631    // Mirror of Python's `Parser.parse_with_recovery` from
1632    // `axon/compiler/parser.py`. Wraps `parse_declaration` in a
1633    // try/recover loop: on any `ParseError` the error is appended to
1634    // the list and the cursor advances to the next sync point, then
1635    // parsing resumes. The two stacks must produce structurally
1636    // identical error lists on the same input — that is the cross-
1637    // stack drift gate (D7). See the test module
1638    // `tests::recovery_tests` and Python-side
1639    // `tests/test_fase28_parser_recovery.py`.
1640
1641    /// Recovery-mode parse. Collects every parse error in source
1642    /// order; the existing `parse()` API remains fail-fast (D9).
1643    ///
1644    /// # Recovery contract (D2)
1645    ///
1646    /// On `ParseError`:
1647    ///   1. Push the error onto `errors`.
1648    ///   2. If the cursor is already on a top-level declaration
1649    ///      keyword (and brace-depth ≤ 0), do not consume — the
1650    ///      caller should retry the declaration parse from here.
1651    ///      Otherwise advance one token to make progress, then
1652    ///      walk to the next sync point.
1653    ///   3. Resume the outer loop.
1654    ///
1655    /// Sync points: top-level declaration keyword at brace-depth ≤ 0,
1656    /// or EOF. Negative depths are treated identically to ≤ 0 — the
1657    /// walker keeps walking through over-balanced `}` rather than
1658    /// pretending a closing brace is itself a sync point (which would
1659    /// emit a ghost "Unexpected token at top level" error in the
1660    /// outer loop).
1661    pub fn parse_with_recovery(&mut self) -> ParseResult {
1662        let mut program = Program {
1663            declarations: Vec::new(),
1664            declaration_trivia: Vec::new(),
1665            loc: Loc { line: 1, column: 1 },
1666        };
1667        let mut errors: Vec<ParseError> = Vec::new();
1668
1669        while !self.check(TokenType::Eof) {
1670            let start_pos = self.pos;
1671            match self.parse_declaration() {
1672                Ok(mut decl) => {
1673                    let end_pos = self.pos.saturating_sub(1);
1674                    let leading = self
1675                        .leading_trivia
1676                        .get(start_pos)
1677                        .cloned()
1678                        .unwrap_or_default();
1679                    let trailing = self
1680                        .trailing_trivia
1681                        .get(end_pos)
1682                        .cloned()
1683                        .unwrap_or_default();
1684                    attach_trivia_to_decl(&mut decl, leading.clone(), trailing.clone());
1685                    program.declarations.push(decl);
1686                    program
1687                        .declaration_trivia
1688                        .push(DeclarationTrivia { leading, trailing });
1689                }
1690                Err(err) => {
1691                    // v1.20.0 — attach source-context block when a
1692                    // source has been provided via `with_source(...)`;
1693                    // otherwise the error keeps its single-line shape.
1694                    errors.push(self.attach_source_to_error(err));
1695                    // Make progress. If parse_declaration returned
1696                    // immediately on the same token (e.g. unknown
1697                    // top-level token), we MUST advance at least one
1698                    // token to avoid an infinite loop.
1699                    if self.pos == start_pos && !self.check(TokenType::Eof) {
1700                        self.advance();
1701                    }
1702                    self.advance_to_sync_point();
1703                }
1704            }
1705        }
1706
1707        ParseResult { program, errors }
1708    }
1709
1710    /// v1.20.0 — Decorate a `ParseError` with a `SourceSnippet`
1711    /// when the parser has source context attached, otherwise return
1712    /// the error unchanged. Idempotent: if the error already carries
1713    /// a snippet, this overwrites it with the parser's source.
1714    fn attach_source_to_error(&self, err: ParseError) -> ParseError {
1715        match &self.source {
1716            Some(src) if err.line >= 1 => err.attach_source(src, &self.filename),
1717            _ => err,
1718        }
1719    }
1720
1721    /// v1.20.0 — Walk the cursor forward until the next sync
1722    /// point (top-level declaration keyword at brace-depth ≤ 0) or
1723    /// EOF. Used by `parse_with_recovery` to skip the malformed
1724    /// remainder of a failed declaration.
1725    fn advance_to_sync_point(&mut self) {
1726        let mut depth: i32 = 0;
1727        while !self.check(TokenType::Eof) {
1728            let tt = self.current().ttype.clone();
1729            // Sync at top-level keywords when depth ≤ 0. We do not
1730            // consume the keyword — the outer loop will dispatch on
1731            // it.
1732            if is_top_level_decl_kw_for_recovery(&tt) && depth <= 0 {
1733                return;
1734            }
1735            if matches!(tt, TokenType::LBrace) {
1736                depth += 1;
1737            } else if matches!(tt, TokenType::RBrace) {
1738                depth -= 1;
1739            }
1740            self.advance();
1741        }
1742    }
1743
1744    // ── token helpers ────────────────────────────────────────────
1745
1746    fn current(&self) -> &Token {
1747        if self.pos >= self.tokens.len() {
1748            self.tokens.last().unwrap() // EOF sentinel
1749        } else {
1750            &self.tokens[self.pos]
1751        }
1752    }
1753
1754    fn advance(&mut self) -> &Token {
1755        let idx = self.pos;
1756        if self.pos < self.tokens.len() {
1757            self.pos += 1;
1758        }
1759        &self.tokens[idx]
1760    }
1761
1762    fn check(&self, tt: TokenType) -> bool {
1763        self.current().ttype == tt
1764    }
1765
1766    fn consume(&mut self, expected: TokenType) -> Result<Token, ParseError> {
1767        let tok = self.current().clone();
1768        if tok.ttype != expected {
1769            return Err(ParseError {
1770                message: format!(
1771                    "Expected {:?}, found {:?}('{}')",
1772                    expected, tok.ttype, tok.value
1773                ),
1774                line: tok.line,
1775                column: tok.column,
1776                            ..Default::default()
1777            });
1778        }
1779        self.pos += 1;
1780        Ok(tok)
1781    }
1782
1783    /// v2.3.0 — build a `ParseError` at the current token's location.
1784    fn error(&self, message: &str) -> ParseError {
1785        let tok = self.current();
1786        ParseError { message: message.to_string(), line: tok.line, column: tok.column, ..Default::default() }
1787    }
1788
1789    /// Consume any identifier or keyword-used-as-value.
1790    fn consume_any_ident_or_kw(&mut self) -> Result<Token, ParseError> {
1791        let tok = self.current().clone();
1792        match tok.ttype {
1793            TokenType::Identifier
1794            | TokenType::Bool
1795            | TokenType::StringLit
1796            | TokenType::Integer
1797            | TokenType::Float => {
1798                self.pos += 1;
1799                Ok(tok)
1800            }
1801            _ => {
1802                // Allow any keyword token whose value is alphabetic
1803                if !tok.value.is_empty()
1804                    && tok.value.chars().all(|c| c.is_alphanumeric() || c == '_')
1805                    && tok.ttype != TokenType::Eof
1806                {
1807                    self.pos += 1;
1808                    Ok(tok)
1809                } else {
1810                    Err(ParseError {
1811                        message: format!(
1812                            "Expected identifier or keyword value, found {:?}('{}')",
1813                            tok.ttype, tok.value
1814                        ),
1815                        line: tok.line,
1816                        column: tok.column,
1817                                            ..Default::default()
1818                    })
1819                }
1820            }
1821        }
1822    }
1823
1824    fn consume_number(&mut self) -> Result<f64, ParseError> {
1825        let tok = self.current().clone();
1826        match tok.ttype {
1827            TokenType::Float | TokenType::Integer => {
1828                self.pos += 1;
1829                tok.value.parse::<f64>().map_err(|_| ParseError {
1830                    message: format!("Invalid number '{}'", tok.value),
1831                    line: tok.line,
1832                    column: tok.column,
1833                                    ..Default::default()
1834                })
1835            }
1836            _ => Err(ParseError {
1837                message: format!("Expected number, found {:?}('{}')", tok.ttype, tok.value),
1838                line: tok.line,
1839                column: tok.column,
1840                            ..Default::default()
1841            }),
1842        }
1843    }
1844
1845    fn parse_bool(&mut self) -> Result<bool, ParseError> {
1846        let tok = self.consume(TokenType::Bool)?;
1847        Ok(tok.value == "true")
1848    }
1849
1850    fn loc_of(&self, tok: &Token) -> Loc {
1851        Loc {
1852            line: tok.line,
1853            column: tok.column,
1854        }
1855    }
1856
1857    fn check_run_modifier(&self) -> bool {
1858        // v2.83.0 — `with <Persona>` is the spelling README uses on every
1859        // `run` it publishes; `as <Persona>` is the one the parser took. Same
1860        // position, same meaning, and the two cannot be confused: `with` is not
1861        // a keyword token, and the only OTHER `with` in the language sits after
1862        // a tool name inside a step body (`use_tool T with k: v`), which this
1863        // predicate is never consulted at.
1864        if self.current().value == "with" {
1865            return true;
1866        }
1867        matches!(
1868            self.current().ttype,
1869            TokenType::As
1870                | TokenType::Within
1871                | TokenType::ConstrainedBy
1872                | TokenType::OnFailure
1873                | TokenType::OutputTo
1874                | TokenType::Effort
1875        )
1876    }
1877
1878    // ── list helpers ─────────────────────────────────────────────
1879
1880    fn parse_string_list(&mut self) -> Result<Vec<String>, ParseError> {
1881        self.consume(TokenType::LBracket)?;
1882        let mut items = Vec::new();
1883        items.push(self.consume(TokenType::StringLit)?.value);
1884        while self.check(TokenType::Comma) {
1885            self.advance();
1886            items.push(self.consume(TokenType::StringLit)?.value);
1887        }
1888        self.consume(TokenType::RBracket)?;
1889        Ok(items)
1890    }
1891
1892    /// v2.38.0 — a bracketed list of quoted string literals, tolerant of
1893    /// an empty `[]` and a trailing comma before `]` (the `Window.exclude`
1894    /// shape, generalized into a reusable helper). Used for CORS field
1895    /// lists whose values contain characters (`://`, `.`, `-`) that aren't
1896    /// valid bare identifiers — `allow_origins`, `allow_headers`,
1897    /// `expose_headers` — where `parse_string_list`'s "at least one item,
1898    /// no trailing comma" strictness would reject a legitimate empty or
1899    /// comma-terminated declaration.
1900    fn parse_bracketed_strings(&mut self) -> Result<Vec<String>, ParseError> {
1901        self.consume(TokenType::LBracket)?;
1902        let mut items = Vec::new();
1903        if !self.check(TokenType::RBracket) {
1904            items.push(self.consume(TokenType::StringLit)?.value);
1905            while self.check(TokenType::Comma) {
1906                self.advance();
1907                if self.check(TokenType::RBracket) {
1908                    break; // trailing comma
1909                }
1910                items.push(self.consume(TokenType::StringLit)?.value);
1911            }
1912        }
1913        self.consume(TokenType::RBracket)?;
1914        Ok(items)
1915    }
1916
1917    fn parse_identifier_list(&mut self) -> Result<Vec<String>, ParseError> {
1918        let mut names = Vec::new();
1919        names.push(self.consume(TokenType::Identifier)?.value);
1920        while self.check(TokenType::Comma) {
1921            self.advance();
1922            names.push(self.consume(TokenType::Identifier)?.value);
1923        }
1924        Ok(names)
1925    }
1926
1927    fn parse_bracketed_identifiers(&mut self) -> Result<Vec<String>, ParseError> {
1928        self.consume(TokenType::LBracket)?;
1929        let items = self.parse_extended_identifier_list()?;
1930        self.consume(TokenType::RBracket)?;
1931        Ok(items)
1932    }
1933
1934    fn parse_extended_identifier_list(&mut self) -> Result<Vec<String>, ParseError> {
1935        let mut items = Vec::new();
1936        items.push(self.consume_any_ident_or_kw()?.value);
1937        while self.check(TokenType::Comma) {
1938            self.advance();
1939            items.push(self.consume_any_ident_or_kw()?.value);
1940        }
1941        Ok(items)
1942    }
1943
1944    fn parse_dotted_identifier(&mut self) -> Result<String, ParseError> {
1945        let mut parts = vec![self.consume_any_ident_or_kw()?.value];
1946        while self.check(TokenType::Dot) {
1947            self.advance();
1948            parts.push(self.consume_any_ident_or_kw()?.value);
1949        }
1950        Ok(parts.join("."))
1951    }
1952
1953    /// v2.83.0 — a **SUBJECT**: the thing a statement acts ON.
1954    ///
1955    /// Every statement in the language has two kinds of operand, and they had
1956    /// been parsed by the same function:
1957    ///
1958    ///   - a **NAME** — the declaration being applied (`compute CalculatePremium`,
1959    ///     `mandate SECFormat`, `use_tool WebSearch`). Always a bare identifier;
1960    ///     a dotted name would refer to nothing.
1961    ///   - a **SUBJECT** — what it acts on (`validate Assess.output`,
1962    ///     `compute X on Analyze.risk_factor, 1.2`). A reference, and a
1963    ///     reference in Axon is DOTTED: `Assess.output` is the canonical way one
1964    ///     step names another's result, and it already parses inside `given:`,
1965    ///     inside `use_tool … with k: v`, and in `navigate_ref`.
1966    ///
1967    /// Subject positions called `consume_any_ident_or_kw`, which stops at the
1968    /// dot. So the reference form the whole language is built on was rejected in
1969    /// exactly the position that most needs it — five README blocks fail on it
1970    /// as their FIRST error and three more need it further in.
1971    ///
1972    /// Literals are subjects too (`compute X on Profile.tenure, 1.2, "USD"`).
1973    /// A string literal keeps its quotes here so the runtime can tell a literal
1974    /// from a binding name — the v2.10.0 classification, preserved instead of
1975    /// flattened.
1976    fn parse_subject(&mut self) -> Result<String, ParseError> {
1977        let t = self.current().clone();
1978        match t.ttype {
1979            TokenType::StringLit => {
1980                self.advance();
1981                Ok(format!("\"{}\"", t.value))
1982            }
1983            TokenType::Integer | TokenType::Float => {
1984                self.advance();
1985                Ok(t.value)
1986            }
1987            _ => self.parse_dotted_identifier(),
1988        }
1989    }
1990
1991    fn parse_expression_string(&mut self) -> Result<String, ParseError> {
1992        if self.check(TokenType::LBracket) {
1993            let items = self.parse_bracketed_dot_identifiers()?;
1994            return Ok(format!("[{}]", items.join(", ")));
1995        }
1996        self.parse_dotted_identifier()
1997    }
1998
1999    fn parse_bracketed_dot_identifiers(&mut self) -> Result<Vec<String>, ParseError> {
2000        self.consume(TokenType::LBracket)?;
2001        let mut items = vec![self.parse_dotted_identifier()?];
2002        while self.check(TokenType::Comma) {
2003            self.advance();
2004            items.push(self.parse_dotted_identifier()?);
2005        }
2006        self.consume(TokenType::RBracket)?;
2007        Ok(items)
2008    }
2009
2010    fn parse_argument_list(&mut self) -> Result<Vec<String>, ParseError> {
2011        let mut args = Vec::new();
2012        while !self.check(TokenType::RParen) {
2013            let tok = self.current().clone();
2014            match tok.ttype {
2015                TokenType::StringLit | TokenType::Integer | TokenType::Float => {
2016                    self.advance();
2017                    args.push(tok.value);
2018                }
2019                TokenType::Identifier => {
2020                    self.advance();
2021                    let mut val = tok.value;
2022                    if self.check(TokenType::Dot) {
2023                        self.advance();
2024                        val.push('.');
2025                        val.push_str(&self.consume_any_ident_or_kw()?.value);
2026                    }
2027                    args.push(val);
2028                }
2029                _ => {
2030                    self.advance();
2031                    let key = tok.value;
2032                    if self.check(TokenType::Colon) {
2033                        self.advance();
2034                        let v = self.advance().value.clone();
2035                        args.push(format!("{key}:{v}"));
2036                    } else {
2037                        args.push(key);
2038                    }
2039                }
2040            }
2041            if self.check(TokenType::Comma) {
2042                self.advance();
2043            }
2044        }
2045        Ok(args)
2046    }
2047
2048    /// Skip a single value or balanced bracketed/braced block (unknown field).
2049    fn skip_value(&mut self) {
2050        match self.current().ttype {
2051            TokenType::LBracket => {
2052                self.advance();
2053                let mut depth = 1u32;
2054                while depth > 0 && !self.check(TokenType::Eof) {
2055                    if self.check(TokenType::LBracket) {
2056                        depth += 1;
2057                    } else if self.check(TokenType::RBracket) {
2058                        depth -= 1;
2059                    }
2060                    self.advance();
2061                }
2062            }
2063            TokenType::LBrace => {
2064                self.advance();
2065                let mut depth = 1u32;
2066                while depth > 0 && !self.check(TokenType::Eof) {
2067                    if self.check(TokenType::LBrace) {
2068                        depth += 1;
2069                    } else if self.check(TokenType::RBrace) {
2070                        depth -= 1;
2071                    }
2072                    self.advance();
2073                }
2074            }
2075            TokenType::Lt => {
2076                // effect row: <io, network, ...>
2077                self.advance();
2078                let mut depth = 1u32;
2079                while depth > 0 && !self.check(TokenType::Eof) {
2080                    if self.check(TokenType::Lt) {
2081                        depth += 1;
2082                    } else if self.check(TokenType::Gt) {
2083                        depth -= 1;
2084                    }
2085                    self.advance();
2086                }
2087            }
2088            _ => {
2089                self.advance();
2090                while self.check(TokenType::Dot) {
2091                    self.advance();
2092                    self.advance();
2093                }
2094            }
2095        }
2096    }
2097
2098    /// Skip a balanced `{ ... }` block including its braces.
2099    fn skip_braced_block(&mut self) -> Result<(), ParseError> {
2100        self.consume(TokenType::LBrace)?;
2101        let mut depth = 1u32;
2102        while depth > 0 {
2103            if self.check(TokenType::Eof) {
2104                let tok = self.current();
2105                return Err(ParseError {
2106                    message: "Unterminated block — expected '}'".to_string(),
2107                    line: tok.line,
2108                    column: tok.column,
2109                                    ..Default::default()
2110                });
2111            }
2112            if self.check(TokenType::LBrace) {
2113                depth += 1;
2114            } else if self.check(TokenType::RBrace) {
2115                depth -= 1;
2116            }
2117            self.advance();
2118        }
2119        Ok(())
2120    }
2121
2122    fn at_declaration_start(&self) -> bool {
2123        is_declaration_keyword(&self.current().ttype) || self.check(TokenType::Eof)
2124    }
2125
2126    // ── top-level dispatch ───────────────────────────────────────
2127
2128    fn parse_declaration(&mut self) -> Result<Declaration, ParseError> {
2129        let tok = self.current().clone();
2130
2131        // v2.69.0 — a TOP-LEVEL `budget <Name> { … }`.
2132        //
2133        // `budget` lexes as `TokenType::Budget` (the daemon-field keyword). At top
2134        // level it is only a declaration when a NAME follows — `budget Foo { … }`.
2135        // The lookahead is what keeps the daemon-attached form (`daemon D { budget
2136        // { … } }`, where `{` follows immediately) untouched: there the next token
2137        // is `{`, not an identifier, so this branch does not fire.
2138        if tok.ttype == TokenType::Budget && self.peek_is_identifier() {
2139            return self.parse_top_level_budget().map(Declaration::Budget);
2140        }
2141
2142        match tok.ttype {
2143            TokenType::Import => self.parse_import().map(Declaration::Import),
2144            TokenType::Persona => self.parse_persona().map(Declaration::Persona),
2145            TokenType::Context => self.parse_context().map(Declaration::Context),
2146            TokenType::Anchor => self.parse_anchor().map(Declaration::Anchor),
2147            TokenType::Memory => self.parse_memory().map(Declaration::Memory),
2148            TokenType::Tool => self.parse_tool().map(Declaration::Tool),
2149            TokenType::Type => self.parse_type_def().map(Declaration::Type),
2150            TokenType::Flow => self.parse_flow().map(Declaration::Flow),
2151            // v2.87.0 — `effect E { Op(p: T) -> R }`. A peer of `tool`, per
2152            // `the design plan` section 3.1 ("top-level, like tool/persona/anchor").
2153            TokenType::Effect => self.parse_effect().map(Declaration::Effect),
2154            TokenType::Intent => self.parse_intent().map(Declaration::Intent),
2155            TokenType::Run => self.parse_run().map(Declaration::Run),
2156            TokenType::Let => self.parse_let().map(Declaration::Let),
2157            TokenType::Know | TokenType::Believe | TokenType::Speculate | TokenType::Doubt => {
2158                self.parse_epistemic_block().map(Declaration::Epistemic)
2159            }
2160            TokenType::Lambda => self.parse_lambda_data().map(Declaration::LambdaData),
2161
2162            // ── Tier 2 declarations (full AST) ──────────────────
2163            TokenType::Agent => self.parse_agent().map(Declaration::Agent),
2164            TokenType::Shield => self.parse_shield().map(Declaration::Shield),
2165            TokenType::Attest => self.parse_attest().map(Declaration::Attest),
2166            // v2.27.0 — temporal execution-window guard.
2167            TokenType::Window => self.parse_window().map(Declaration::Window),
2168            TokenType::Pix => self.parse_pix().map(Declaration::Pix),
2169            TokenType::Ledger => self.parse_ledger().map(Declaration::Ledger),
2170            TokenType::Psyche => self.parse_psyche().map(Declaration::Psyche),
2171            TokenType::Corpus => self.parse_corpus().map(Declaration::Corpus),
2172            TokenType::Dataspace => self.parse_dataspace().map(Declaration::Dataspace),
2173            TokenType::Ots => self.parse_ots().map(Declaration::Ots),
2174            TokenType::Mandate => self.parse_mandate().map(Declaration::Mandate),
2175            TokenType::Compute => self.parse_compute().map(Declaration::Compute),
2176            TokenType::Daemon => self.parse_daemon().map(Declaration::Daemon),
2177            TokenType::Extension => self.parse_extension().map(Declaration::Extension),
2178            TokenType::AxonStore => self.parse_axonstore().map(Declaration::AxonStore),
2179            TokenType::AxonEndpoint => self.parse_axonendpoint().map(Declaration::AxonEndpoint),
2180
2181            // ── v1.1.0 — I/O cognitivo ───────────────────
2182            TokenType::Resource => self.parse_resource().map(Declaration::Resource),
2183            TokenType::Fabric => self.parse_fabric().map(Declaration::Fabric),
2184            TokenType::Manifest => self.parse_manifest().map(Declaration::Manifest),
2185            TokenType::Observe => self.parse_observe().map(Declaration::Observe),
2186
2187            // ── v1.1.0 — Control cognitivo ───────────────
2188            TokenType::Reconcile => self.parse_reconcile().map(Declaration::Reconcile),
2189            TokenType::Lease => self.parse_lease().map(Declaration::Lease),
2190            TokenType::Ensemble => self.parse_ensemble().map(Declaration::Ensemble),
2191
2192            // ── v1.1.0 — Topology + π-calculus sessions ─
2193            TokenType::Session => self.parse_session_definition().map(Declaration::Session),
2194            TokenType::Topology => self.parse_topology().map(Declaration::Topology),
2195
2196            // ── v2.3.0 — typed WebSocket transport ─────────
2197            TokenType::Socket => self.parse_socket().map(Declaration::Socket),
2198
2199            // ── v2.37.0 — outbound vendor connection ─────────
2200            TokenType::Upstream => self.parse_upstream().map(Declaration::Upstream),
2201
2202            // ── v2.37.0 — the voice-agent simplicity layer ───
2203            TokenType::Voice => self.parse_voice().map(Declaration::Voice),
2204
2205            // ── v2.38.0 — the named origin-policy declaration ─
2206            TokenType::Cors => self.parse_cors().map(Declaration::Cors),
2207
2208            // ── v2.40.0 — the named result-memoization policy ─
2209            TokenType::Cache => self.parse_cache().map(Declaration::Cache),
2210            TokenType::Document => self.parse_document().map(Declaration::Document),
2211
2212            // ── v2.60.0 — Governed CRM Delivery ─
2213            TokenType::Deliver => self.parse_deliver().map(Declaration::Deliver),
2214            TokenType::Notify => self.parse_notify().map(Declaration::Notify),
2215
2216            // ── v2.42.0 — the long-horizon autonomous research primitive ─
2217            TokenType::Savant => self.parse_savant().map(Declaration::Savant),
2218
2219            // ── v2.42.0 — the dynamic tool-synthesis policy ──────────────
2220            TokenType::Synth => self.parse_synth().map(Declaration::Synth),
2221
2222            // ── v2.43.0 — the authorization-scope policy declaration ─────
2223            TokenType::Scope => self.parse_scope().map(Declaration::Scope),
2224
2225            // ── v2.46.0 — the ephemeral-credential contract ──────────────
2226            TokenType::Credential => self.parse_credential().map(Declaration::Credential),
2227
2228            // ── v2.4.0 — Pauli-sum observable ────────────
2229            TokenType::Observable => self.parse_observable().map(Declaration::Observable),
2230
2231            // ── v2.23.0 — Advantage Witness ──────────────────
2232            TokenType::Witness => self.parse_witness().map(Declaration::Witness),
2233
2234            // ── v1.1.0 — Cognitive immune system ─────────
2235            TokenType::Immune => self.parse_immune().map(Declaration::Immune),
2236            TokenType::Reflex => self.parse_reflex().map(Declaration::Reflex),
2237            TokenType::Heal => self.parse_heal().map(Declaration::Heal),
2238
2239            // ── v1.3.1 — UI cognitiva ────────────────────
2240            TokenType::Component => self.parse_component().map(Declaration::Component),
2241            TokenType::View => self.parse_view().map(Declaration::View),
2242
2243            // ── v1.6.0 — Mobile typed channels ──────────
2244            TokenType::Channel => self.parse_channel().map(Declaration::Channel),
2245
2246            // ── Tier 3+ structural fallback ─────────────────────
2247            // Store operations: keyword target { ... } or keyword target ...
2248            TokenType::Ingest
2249            | TokenType::Persist
2250            | TokenType::Retrieve
2251            | TokenType::Mutate
2252            | TokenType::Purge
2253            | TokenType::Transact => self.parse_generic_declaration(),
2254
2255            // MCP declaration
2256            TokenType::Mcp => self.parse_generic_declaration(),
2257
2258            _ => {
2259                // v1.20.0 — append "Did you mean X?" hint when the
2260                // unknown token looks like a typo'd top-level keyword
2261                // (Levenshtein ≤ 2). D3, D11 ratified 2026-05-10.
2262                let hint = crate::smart_suggest::suggest_for(
2263                    &tok.value,
2264                    crate::smart_suggest::TOP_LEVEL_KEYWORD_NAMES,
2265                );
2266                let base = format!(
2267                    "Unexpected token at top level: '{}' — expected declaration \
2268                     (persona, context, anchor, flow, run, ...)",
2269                    tok.value
2270                );
2271                let message = if hint.is_empty() {
2272                    base
2273                } else {
2274                    format!("{base}. {hint}")
2275                };
2276                Err(ParseError {
2277                    message,
2278                    line: tok.line,
2279                    column: tok.column,
2280                    ..Default::default()
2281                })
2282            }
2283        }
2284    }
2285
2286    // ── IMPORT ───────────────────────────────────────────────────
2287
2288    fn parse_import(&mut self) -> Result<ImportNode, ParseError> {
2289        let tok = self.consume(TokenType::Import)?;
2290        let loc = self.loc_of(&tok);
2291
2292        let mut path_parts = Vec::new();
2293
2294        // Optional @ scope
2295        if self.check(TokenType::At) {
2296            self.advance();
2297            let first = self.consume(TokenType::Identifier)?;
2298            path_parts.push(format!("@{}", first.value));
2299        } else {
2300            let first = self.consume(TokenType::Identifier)?;
2301            path_parts.push(first.value);
2302        }
2303
2304        while self.check(TokenType::Dot) {
2305            self.advance();
2306            if self.check(TokenType::LBrace) {
2307                break;
2308            }
2309            let part = self.consume(TokenType::Identifier)?;
2310            path_parts.push(part.value);
2311        }
2312
2313        let mut names = Vec::new();
2314        if self.check(TokenType::LBrace) {
2315            self.advance();
2316            names = self.parse_identifier_list()?;
2317            self.consume(TokenType::RBrace)?;
2318        }
2319
2320        // ── v2.76.0 — the `@allow_downgrade` ECC valve ───────────────
2321        //
2322        // `import a.b.{X} @allow_downgrade` acknowledges an epistemic
2323        // downgrade across this edge (see `epistemic_compat.rs`). The
2324        // annotation position is unambiguous: no top-level declaration
2325        // begins with `@`, so an `@` here belongs to this import — and an
2326        // unknown annotation is refused with the fix in the message
2327        // rather than surfacing later as an opaque parse error.
2328        let mut allow_downgrade = false;
2329        if self.check(TokenType::At) {
2330            let at_tok = self.current().clone();
2331            self.advance();
2332            let ident = self.consume(TokenType::Identifier)?;
2333            if ident.value == "allow_downgrade" {
2334                allow_downgrade = true;
2335            } else {
2336                return Err(ParseError {
2337                    message: format!(
2338                        "unknown import annotation '@{}' — the only import annotation is \
2339                         `@allow_downgrade` (the epistemic-downgrade acknowledgment).",
2340                        ident.value
2341                    ),
2342                    line: at_tok.line,
2343                    column: at_tok.column,
2344                    ..Default::default()
2345                });
2346            }
2347        }
2348
2349        // ── v2.67.0 — `apx` is RETRACTED ───────────────────────────────
2350        //
2351        // `import X with apx { … }` used to parse and then call
2352        // `skip_braced_block()` — the policy was consumed and thrown on the
2353        // floor. It never reached the AST, let alone the IR. In `axon-rs` the
2354        // string "apx" occurred only inside comments: there is no APX crate,
2355        // no binary, no MEC/PCC dependency verification, no EPR ranking, no
2356        // quarantine and no compliance gate. The public README advertised all
2357        // five.
2358        //
2359        // A dependency policy that silently evaporates is the worst possible
2360        // shape for this particular promise: the adopter believes their supply
2361        // chain is being verified, which is exactly the belief that stops them
2362        // from verifying it themselves. Refuse, loudly.
2363        let next_is_apx = self
2364            .tokens
2365            .get(self.pos + 1)
2366            .map(|t| t.value == "apx")
2367            .unwrap_or(false);
2368        if self.current().value == "with" && next_is_apx {
2369            let tok = self.current().clone();
2370            return Err(ParseError {
2371                message: "`import … with apx { … }` is RETRACTED (v2.67.0). The apx policy block was \
2372                          parsed and silently DISCARDED — it never reached the IR, and no epistemic \
2373                          dependency manager exists: no MEC/PCC verification, no EPR ranking, no \
2374                          quarantine, no compliance gate. Declaring it verified nothing while \
2375                          implying your supply chain was checked. Remove the `with apx { … }` \
2376                          clause; the plain `import` resolves through the Epistemic Module \
2377                          System."
2378                    .to_string(),
2379                line: tok.line,
2380                column: tok.column,
2381                ..Default::default()
2382            });
2383        }
2384
2385        Ok(ImportNode {
2386            module_path: path_parts,
2387            names,
2388            allow_downgrade,
2389            loc,
2390            leading_trivia: Vec::new(),
2391            trailing_trivia: Vec::new(),
2392        })
2393    }
2394
2395    // ── PERSONA ──────────────────────────────────────────────────
2396
2397    fn parse_persona(&mut self) -> Result<PersonaDefinition, ParseError> {
2398        let tok = self.consume(TokenType::Persona)?;
2399        let loc = self.loc_of(&tok);
2400        let name = self.consume(TokenType::Identifier)?.value;
2401        self.consume(TokenType::LBrace)?;
2402
2403        let mut node = PersonaDefinition {
2404            name,
2405            domain: Vec::new(),
2406            tone: String::new(),
2407            confidence_threshold: None,
2408            cite_sources: None,
2409            refuse_if: Vec::new(),
2410            language: String::new(),
2411            description: String::new(),
2412            loc,
2413            leading_trivia: Vec::new(),
2414            trailing_trivia: Vec::new(),
2415        };
2416
2417        while !self.check(TokenType::RBrace) {
2418            let field_name = self.current().value.clone();
2419            self.advance();
2420            self.consume(TokenType::Colon)?;
2421
2422            match field_name.as_str() {
2423                "domain" => node.domain = self.parse_string_list()?,
2424                "tone" => node.tone = self.consume_any_ident_or_kw()?.value,
2425                "confidence_threshold" => node.confidence_threshold = Some(self.consume_number()?),
2426                "cite_sources" => node.cite_sources = Some(self.parse_bool()?),
2427                "refuse_if" => node.refuse_if = self.parse_bracketed_identifiers()?,
2428                "language" => node.language = self.consume(TokenType::StringLit)?.value,
2429                "description" => node.description = self.consume(TokenType::StringLit)?.value,
2430                _ => self.skip_value(),
2431            }
2432        }
2433        self.consume(TokenType::RBrace)?;
2434        Ok(node)
2435    }
2436
2437    // ── CONTEXT ──────────────────────────────────────────────────
2438
2439    fn parse_context(&mut self) -> Result<ContextDefinition, ParseError> {
2440        let tok = self.consume(TokenType::Context)?;
2441        let loc = self.loc_of(&tok);
2442        let name = self.consume(TokenType::Identifier)?.value;
2443        self.consume(TokenType::LBrace)?;
2444
2445        let mut node = ContextDefinition {
2446            name,
2447            memory_scope: String::new(),
2448            language: String::new(),
2449            depth: String::new(),
2450            max_tokens: None,
2451            temperature: None,
2452            cite_sources: None,
2453            now_tz: None,
2454            loc,
2455            leading_trivia: Vec::new(),
2456            trailing_trivia: Vec::new(),
2457        };
2458
2459        while !self.check(TokenType::RBrace) {
2460            let field_name = self.current().value.clone();
2461            self.advance();
2462            self.consume(TokenType::Colon)?;
2463
2464            match field_name.as_str() {
2465                "memory" => node.memory_scope = self.consume_any_ident_or_kw()?.value,
2466                "language" => node.language = self.consume(TokenType::StringLit)?.value,
2467                "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
2468                // v2.46.0 — the frame's cognitive timezone (IANA string).
2469                "now" => node.now_tz = Some(self.consume(TokenType::StringLit)?.value),
2470                "max_tokens" => {
2471                    node.max_tokens = Some(
2472                        self.consume(TokenType::Integer)?
2473                            .value
2474                            .parse::<i64>()
2475                            .unwrap_or(0),
2476                    )
2477                }
2478                "temperature" => node.temperature = Some(self.consume_number()?),
2479                "cite_sources" => node.cite_sources = Some(self.parse_bool()?),
2480                _ => self.skip_value(),
2481            }
2482        }
2483        self.consume(TokenType::RBrace)?;
2484        Ok(node)
2485    }
2486
2487    // ── ANCHOR ───────────────────────────────────────────────────
2488
2489    fn parse_anchor(&mut self) -> Result<AnchorConstraint, ParseError> {
2490        let tok = self.consume(TokenType::Anchor)?;
2491        let loc = self.loc_of(&tok);
2492        let name = self.consume(TokenType::Identifier)?.value;
2493        self.consume(TokenType::LBrace)?;
2494
2495        let mut node = AnchorConstraint {
2496            name,
2497            require: String::new(),
2498            reject: Vec::new(),
2499            enforce: String::new(),
2500            description: String::new(),
2501            confidence_floor: None,
2502            unknown_response: String::new(),
2503            on_violation: String::new(),
2504            on_violation_target: String::new(),
2505            loc,
2506            leading_trivia: Vec::new(),
2507            trailing_trivia: Vec::new(),
2508        };
2509
2510        while !self.check(TokenType::RBrace) {
2511            let field_name = self.current().value.clone();
2512            self.advance();
2513            self.consume(TokenType::Colon)?;
2514
2515            match field_name.as_str() {
2516                "require" => node.require = self.consume_any_ident_or_kw()?.value,
2517                "description" => node.description = self.consume(TokenType::StringLit)?.value,
2518                "reject" => node.reject = self.parse_bracketed_identifiers()?,
2519                "enforce" => node.enforce = self.consume_any_ident_or_kw()?.value,
2520                "confidence_floor" => node.confidence_floor = Some(self.consume_number()?),
2521                "unknown_response" => {
2522                    node.unknown_response = self.consume(TokenType::StringLit)?.value
2523                }
2524                "on_violation" => {
2525                    // Parse: raise ErrorName | fallback(...) | identifier
2526                    let action = self.consume_any_ident_or_kw()?.value;
2527                    node.on_violation = action.clone();
2528                    if action == "raise" || action == "fallback" {
2529                        node.on_violation_target = self.consume_any_ident_or_kw()?.value;
2530                    }
2531                }
2532                _ => self.skip_value(),
2533            }
2534        }
2535        self.consume(TokenType::RBrace)?;
2536        Ok(node)
2537    }
2538
2539    // ── MEMORY ───────────────────────────────────────────────────
2540
2541    fn parse_memory(&mut self) -> Result<MemoryDefinition, ParseError> {
2542        let tok = self.consume(TokenType::Memory)?;
2543        let loc = self.loc_of(&tok);
2544        let name = self.consume(TokenType::Identifier)?.value;
2545        self.consume(TokenType::LBrace)?;
2546
2547        let mut node = MemoryDefinition {
2548            name,
2549            store: String::new(),
2550            backend: String::new(),
2551            retrieval: String::new(),
2552            decay: String::new(),
2553            loc,
2554            leading_trivia: Vec::new(),
2555            trailing_trivia: Vec::new(),
2556        };
2557
2558        while !self.check(TokenType::RBrace) {
2559            let field_name = self.current().value.clone();
2560            self.advance();
2561            self.consume(TokenType::Colon)?;
2562
2563            match field_name.as_str() {
2564                "store" => node.store = self.consume_any_ident_or_kw()?.value,
2565                "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
2566                "retrieval" => node.retrieval = self.consume_any_ident_or_kw()?.value,
2567                "decay" => {
2568                    if self.check(TokenType::Duration) {
2569                        node.decay = self.advance().value.clone();
2570                    } else {
2571                        node.decay = self.consume_any_ident_or_kw()?.value;
2572                    }
2573                }
2574                _ => self.skip_value(),
2575            }
2576        }
2577        self.consume(TokenType::RBrace)?;
2578        Ok(node)
2579    }
2580
2581    // ── TOOL ─────────────────────────────────────────────────────
2582
2583    fn parse_tool(&mut self) -> Result<ToolDefinition, ParseError> {
2584        let tok = self.consume(TokenType::Tool)?;
2585        let loc = self.loc_of(&tok);
2586        let name = self.consume(TokenType::Identifier)?.value;
2587        self.consume(TokenType::LBrace)?;
2588
2589        let mut node = ToolDefinition {
2590            shield_ref: String::new(),
2591            name,
2592            provider: String::new(),
2593            max_results: None,
2594            filter_expr: String::new(),
2595            timeout: String::new(),
2596            runtime: String::new(),
2597            resource_ref: String::new(),
2598            sandbox: None,
2599            effects: None,
2600            parameters: Vec::new(),
2601            output_type: None,
2602            requires: Vec::new(),
2603            secret: String::new(),
2604            secret_partition: String::new(),
2605            target: None,
2606            risk: None,
2607            argv: Vec::new(),
2608            cache: String::new(),
2609            scrape: None,
2610            loc,
2611            leading_trivia: Vec::new(),
2612            trailing_trivia: Vec::new(),
2613        };
2614
2615        // v2.39.0/the design decision — unknown fields are recorded (not silently
2616        // skipped) so a `target:`-bound technician tool can HARD-ERROR on one
2617        // (a typo'd safety field must never quietly disable a guard), while a
2618        // legacy schema-less tool keeps its lenient record-and-skip (zero
2619        // regression). The decision is deferred to after the block is parsed,
2620        // since `target:` may appear after the unknown field.
2621        let mut unknown_fields: Vec<(String, u32, u32)> = Vec::new();
2622
2623        while !self.check(TokenType::RBrace) {
2624            let field_tok = self.current().clone();
2625            let field_name = field_tok.value.clone();
2626            self.advance();
2627            self.consume(TokenType::Colon)?;
2628
2629            match field_name.as_str() {
2630                "provider" => node.provider = self.consume_any_ident_or_kw()?.value,
2631                "max_results" => {
2632                    node.max_results = Some(
2633                        self.consume(TokenType::Integer)?
2634                            .value
2635                            .parse::<i64>()
2636                            .unwrap_or(0),
2637                    )
2638                }
2639                "filter" => node.filter_expr = self.parse_filter_expression()?,
2640                "timeout" => node.timeout = self.consume(TokenType::Duration)?.value,
2641                "runtime" => node.runtime = self.consume_any_ident_or_kw()?.value,
2642                // v2.69.0 — the `resource` this tool's channel runs on. The
2643                // channel's address, concurrency and lifecycle come from it;
2644                // `runtime:` then names the path within the channel.
2645                "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
2646                "sandbox" => node.sandbox = Some(self.parse_bool()?),
2647                "effects" => node.effects = Some(self.parse_effect_row()?),
2648                // v2.8.0 — the tool's typed input schema + output type.
2649                "parameters" => node.parameters = self.parse_tool_param_schema()?,
2650                "output_type" => node.output_type = Some(self.parse_output_type_string()?),
2651                // v2.77.0 — the tool's required authorization
2652                // scopes: bare dot-separated capability slugs, the EXACT
2653                // grammar + charset of `credential.grants` (v2.46.0) so the two
2654                // vocabularies are one. `requires: [w_organization_social,
2655                // video.publish]`. Subset coverage is `axon-T956`.
2656                "requires" => {
2657                    let bracket_tok = self.current().clone();
2658                    let items = self.parse_bracketed_dot_identifiers()?;
2659                    for slug in &items {
2660                        if !is_valid_capability_slug(slug) {
2661                            return Err(ParseError {
2662                                message: format!(
2663                                    "Invalid capability slug '{slug}' in tool '{}' \
2664                                     `requires:`. Scope slugs must match \
2665                                     ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — the same \
2666                                     grammar as `credential.grants`. Examples: \
2667                                     `w_organization_social`, `video.publish`.",
2668                                    node.name
2669                                ),
2670                                line: bracket_tok.line,
2671                                column: bracket_tok.column,
2672                                ..Default::default()
2673                            });
2674                        }
2675                    }
2676                    node.requires = items;
2677                }
2678                // v2.48.0 — the per-tenant secret KEY injected at
2679                // dispatch (`rotation_without_revelation`). Key shape +
2680                // technician exclusion are `axon-T902` (type-checker).
2681                "secret" => node.secret = self.parse_dotted_identifier()?,
2682                // v4.3.0 — the control that covers this tool's κ (axon-T1221).
2683                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
2684                // v2.49.0 — `secret_partition:` names one of this tool's
2685                // own `parameters:` (a bare identifier, NOT dotted — it is a
2686                // parameter reference, not a key). Its runtime value becomes
2687                // a single appended key segment at dispatch. The membership +
2688                // `String`-type + technician laws are `axon-T903`.
2689                "secret_partition" => {
2690                    node.secret_partition = self.consume_any_ident_or_kw()?.value
2691                }
2692                // v2.39.0 — Remote Hands technician fields.
2693                "target" => node.target = Some(self.consume_any_ident_or_kw()?.value),
2694                "risk" => node.risk = Some(self.consume_any_ident_or_kw()?.value),
2695                // The argv template: a bracketed list of quoted elements
2696                // (`argv: ["ping", "-c", "${count}", "${host}"]`). Reuses the
2697                // CORS list helper (tolerant of `[]` and a trailing comma).
2698                "argv" => node.argv = self.parse_bracketed_strings()?,
2699                // v2.40.0 — the tool's result-memoization policy reference
2700                // (a declared `cache` name, or the `none` opt-out sentinel).
2701                "cache" => node.cache = self.consume_any_ident_or_kw()?.value,
2702                // v2.52.0 — the closed-catalog web-acquisition config
2703                // block. `scrape: { engine: …, extract: […], … }`.
2704                "scrape" => node.scrape = Some(self.parse_scrape_spec()?),
2705                _ => {
2706                    unknown_fields.push((field_name, field_tok.line, field_tok.column));
2707                    self.skip_value();
2708                }
2709            }
2710        }
2711        self.consume(TokenType::RBrace)?;
2712
2713        // v2.39.0/the design decision — a `target:`-bound tool opts into strict field
2714        // checking. An unknown field on it is a parse error, mirroring the v2.38.0
2715        // `cors`/`voice` closed-catalog discipline — but scoped to the
2716        // technician surface so ordinary tools are untouched.
2717        // v2.52.0 — a `scrape:`-bearing web-acquisition tool opts
2718        // into the same strictness: a typo'd safety field (e.g. a mis-spelled
2719        // `respect_robots`) must never quietly disable a guard.
2720        if node.target.is_some() || node.scrape.is_some() {
2721            if let Some((field_name, line, column)) = unknown_fields.into_iter().next() {
2722                let (surface, valid) = if node.target.is_some() {
2723                    (
2724                        "technician tool",
2725                        "provider, parameters, output_type, timeout, effects, target, risk, argv",
2726                    )
2727                } else {
2728                    (
2729                        "web-acquisition tool",
2730                        "provider, parameters, output_type, timeout, effects, secret, \
2731                         secret_partition, cache, scrape",
2732                    )
2733                };
2734                return Err(ParseError {
2735                    message: format!(
2736                        "unknown field `{field_name}` in {surface} `{}` — this tool uses \
2737                         strict field checking; valid fields: {valid}",
2738                        node.name
2739                    ),
2740                    line,
2741                    column,
2742                    ..Default::default()
2743                });
2744            }
2745        }
2746        Ok(node)
2747    }
2748
2749    /// v2.52.0 — parse the closed-catalog `scrape: { … }` web-acquisition
2750    /// config sub-block. Every field is optional; an unknown field is a hard
2751    /// parse error (the v2.38.0 `cors` closed-catalog discipline). Mirrors the
2752    /// field grammar of `parse_tool` for the scrape-specific keys.
2753    fn parse_scrape_spec(&mut self) -> Result<crate::ast::ScrapeSpec, ParseError> {
2754        let open = self.consume(TokenType::LBrace)?;
2755        let loc = self.loc_of(&open);
2756        let mut spec = crate::ast::ScrapeSpec {
2757            loc,
2758            ..Default::default()
2759        };
2760        while !self.check(TokenType::RBrace) {
2761            let field_tok = self.current().clone();
2762            let field_name = field_tok.value.clone();
2763            self.advance();
2764            self.consume(TokenType::Colon)?;
2765            match field_name.as_str() {
2766                "engine" => spec.engine = Some(self.consume_any_ident_or_kw()?.value),
2767                "impersonate" => spec.impersonate = Some(self.consume_any_ident_or_kw()?.value),
2768                "render_wait" => spec.render_wait = Some(self.consume(TokenType::Duration)?.value),
2769                "proxy" => spec.proxy = self.parse_dotted_identifier()?,
2770                "respect_robots" => spec.respect_robots = Some(self.parse_bool()?),
2771                "extract" => spec.extract = self.parse_bracketed_strings()?,
2772                "adaptive" => spec.adaptive = Some(self.parse_bool()?),
2773                "similarity_floor" => spec.similarity_floor = self.parse_optional_float(),
2774                "follow" => spec.follow = self.consume(TokenType::StringLit)?.value,
2775                "max_depth" => {
2776                    spec.max_depth =
2777                        Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2778                }
2779                "max_pages" => {
2780                    spec.max_pages =
2781                        Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2782                }
2783                "concurrency" => {
2784                    spec.concurrency =
2785                        Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2786                }
2787                "politeness" => spec.politeness = self.consume_any_ident_or_kw()?.value,
2788                "checkpoint" => spec.checkpoint = self.consume_any_ident_or_kw()?.value,
2789                other => {
2790                    return Err(self.error(&format!(
2791                        "unknown scrape field `{other}` — the `scrape: {{ … }}` block is a \
2792                         closed catalog; valid fields: engine, impersonate, \
2793                         render_wait, proxy, respect_robots, extract, adaptive, \
2794                         similarity_floor, follow, max_depth, max_pages, concurrency, \
2795                         politeness, checkpoint"
2796                    )));
2797                }
2798            }
2799        }
2800        self.consume(TokenType::RBrace)?;
2801        Ok(spec)
2802    }
2803
2804    /// v2.8.0 — parse a tool's INPUT SCHEMA: a brace-delimited list of
2805    /// `name: Type` parameters (`parameters: { query: String, max_results: Int }`).
2806    /// Reuses the flow-parameter shape (`Parameter`), so the same `TypeExpr`
2807    /// grammar — generics like `List<T>`, `?`-optionals — applies. A trailing
2808    /// comma is tolerated; an empty `{}` yields no parameters.
2809    fn parse_tool_param_schema(&mut self) -> Result<Vec<Parameter>, ParseError> {
2810        self.consume(TokenType::LBrace)?;
2811        let mut params = Vec::new();
2812        while !self.check(TokenType::RBrace) {
2813            // Accept a keyword-as-name (`filter`, `type`, `domain`, …) — real
2814            // adopter tool schemas use such parameter names; the `:` after it
2815            // disambiguates.
2816            let name = self.consume_any_ident_or_kw()?;
2817            let ploc = self.loc_of(&name);
2818            self.consume(TokenType::Colon)?;
2819            let type_expr = self.parse_type_expr()?;
2820            params.push(Parameter {
2821                name: name.value,
2822                type_expr,
2823                loc: ploc,
2824            });
2825            if self.check(TokenType::Comma) {
2826                self.advance();
2827            } else {
2828                break;
2829            }
2830        }
2831        self.consume(TokenType::RBrace)?;
2832        Ok(params)
2833    }
2834
2835    fn parse_filter_expression(&mut self) -> Result<String, ParseError> {
2836        let name = self.consume_any_ident_or_kw()?.value;
2837        if self.check(TokenType::LParen) {
2838            self.advance();
2839            let mut parts = vec![name, "(".to_string()];
2840            while !self.check(TokenType::RParen) {
2841                parts.push(self.advance().value.clone());
2842            }
2843            self.consume(TokenType::RParen)?;
2844            parts.push(")".to_string());
2845            Ok(parts.join(""))
2846        } else {
2847            Ok(name)
2848        }
2849    }
2850
2851    fn parse_effect_row(&mut self) -> Result<EffectRow, ParseError> {
2852        let tok = self.consume(TokenType::Lt)?;
2853        let loc = self.loc_of(&tok);
2854        let mut effects = Vec::new();
2855        let mut epistemic_level = String::new();
2856
2857        while !self.check(TokenType::Gt) {
2858            let name = self.consume_any_ident_or_kw()?.value;
2859            if self.check(TokenType::Colon) {
2860                self.advance();
2861                // v1.4.0 — qualifiers can be compound slugs
2862                // from a closed catalogue:
2863                //
2864                //   * dot-separated  — `legal:HIPAA.164_502`,
2865                //                       `legal:GDPR.Art6.Consent`,
2866                //                       `legal:PCI_DSS.v4_Req3`
2867                //   * colon-separated — `ots:transform:mulaw8:pcm16`,
2868                //                       `ots:backend:native`
2869                //   * mixed           — supported by the same loop.
2870                //
2871                // The lexer fragments dotted slugs across IDENT /
2872                // INTEGER tokens (e.g., `164_502` lexes as INTEGER
2873                // `164` + IDENT `_502` because `_` starts a fresh
2874                // identifier); we recombine here using source-column
2875                // adjacency so the type checker sees the catalog
2876                // string verbatim.
2877                let level = self.parse_qualifier_value()?;
2878                if name == "epistemic" {
2879                    epistemic_level = level;
2880                } else {
2881                    effects.push(format!("{name}:{level}"));
2882                }
2883            } else {
2884                effects.push(name);
2885            }
2886            if self.check(TokenType::Comma) {
2887                self.advance();
2888            }
2889        }
2890        self.consume(TokenType::Gt)?;
2891
2892        Ok(EffectRow {
2893            effects,
2894            epistemic_level,
2895            loc,
2896        })
2897    }
2898
2899    /// Parse a compound qualifier value following an effect's first
2900    /// colon — supports both dot-separated (`HIPAA.164_502`) and
2901    /// colon-separated (`transform:mulaw8:pcm16`) catalogue slugs, as
2902    /// well as mixed forms.
2903    ///
2904    /// The grammar is: `segment ((`.` | `:`) segment)*` where a
2905    /// segment is a contiguous run of IDENT / INTEGER tokens (see
2906    /// [`Self::consume_dotted_slug_segment`]).
2907    fn parse_qualifier_value(&mut self) -> Result<String, ParseError> {
2908        let mut buf = self.consume_dotted_slug_segment()?;
2909        loop {
2910            let sep = if self.check(TokenType::Dot) {
2911                '.'
2912            } else if self.check(TokenType::Colon) {
2913                ':'
2914            } else {
2915                break;
2916            };
2917            self.advance();
2918            let part = self.consume_dotted_slug_segment()?;
2919            buf.push(sep);
2920            buf.push_str(&part);
2921        }
2922        Ok(buf)
2923    }
2924
2925    /// Consume a contiguous run of IDENT / INTEGER / keyword-ident
2926    /// tokens whose source positions are adjacent (no whitespace
2927    /// between them), concatenating their text into a single segment.
2928    ///
2929    /// Needed for closed-catalogue qualifier slugs whose segment
2930    /// mixes digits and identifier characters — e.g. `HIPAA.164_502`
2931    /// lexes as INTEGER `164` + IDENT `_502` because `_` starts a
2932    /// fresh identifier; the catalog value is the concatenation
2933    /// `164_502`. Adjacency is determined by matching
2934    /// `(line, column + len)` of the previous token against the next
2935    /// token's start position.
2936    fn consume_dotted_slug_segment(&mut self) -> Result<String, ParseError> {
2937        let first = self.consume_any_ident_or_kw()?;
2938        let mut buf = first.value.clone();
2939        let mut next_line = first.line;
2940        let mut next_col = first.column + first.value.chars().count() as u32;
2941        loop {
2942            let cur = self.current();
2943            let is_segment_token = matches!(cur.ttype, TokenType::Identifier | TokenType::Integer,);
2944            if !is_segment_token {
2945                break;
2946            }
2947            if cur.line != next_line || cur.column != next_col {
2948                break;
2949            }
2950            buf.push_str(&cur.value);
2951            next_col = cur.column + cur.value.chars().count() as u32;
2952            next_line = cur.line;
2953            self.pos += 1;
2954        }
2955        Ok(buf)
2956    }
2957
2958    // ── TYPE ─────────────────────────────────────────────────────
2959
2960    fn parse_type_def(&mut self) -> Result<TypeDefinition, ParseError> {
2961        let tok = self.consume(TokenType::Type)?;
2962        let loc = self.loc_of(&tok);
2963        let name = self.consume(TokenType::Identifier)?.value;
2964
2965        let mut node = TypeDefinition {
2966            identifier: String::new(),
2967            name,
2968            fields: Vec::new(),
2969            range_constraint: None,
2970            where_clause: None,
2971            compliance: Vec::new(),
2972            loc: loc.clone(),
2973            leading_trivia: Vec::new(),
2974            trailing_trivia: Vec::new(),
2975        };
2976
2977        // Optional range: (0.0..1.0)
2978        if self.check(TokenType::LParen) {
2979            self.advance();
2980            let min_val = self.consume_number()?;
2981            self.consume(TokenType::DotDot)?;
2982            let max_val = self.consume_number()?;
2983            self.consume(TokenType::RParen)?;
2984            node.range_constraint = Some(RangeConstraint {
2985                min_value: min_val,
2986                max_value: max_val,
2987                loc: loc.clone(),
2988            });
2989        }
2990
2991        // Optional where clause
2992        if self.check(TokenType::Where) {
2993            self.advance();
2994            let mut expr_parts = Vec::new();
2995            while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
2996                if self.check(TokenType::Eof) {
2997                    break;
2998                }
2999                expr_parts.push(self.advance().value.clone());
3000            }
3001            node.where_clause = Some(WhereClause {
3002                expression: expr_parts.join(" "),
3003                loc: loc.clone(),
3004            });
3005        }
3006
3007        // Optional ESK — `compliance [HIPAA, ...]` prefix modifier
3008        // between `type Name` / `range` / `where` and the body `{`.
3009        if self.check(TokenType::Identifier) && self.current().value == "compliance" {
3010            self.advance();
3011            node.compliance = self.parse_bracketed_identifiers()?;
3012        }
3013
3014        // v4.5.0 — `identifier <kind>`: WHAT this type is, from the closed
3015        // catalogue. It sits beside `compliance` because the two answer
3016        // different questions about the same declaration — which regime, and
3017        // which kind of thing — and a de-identification rule needs both.
3018        //
3019        // Accepted in either order with `compliance`, because insisting on one
3020        // would be a rule an adopter has to remember for no reason.
3021        if self.check(TokenType::Identifier) && self.current().value == "identifier" {
3022            self.advance();
3023            node.identifier = self.consume_any_ident_or_kw()?.value.clone();
3024            if self.check(TokenType::Identifier) && self.current().value == "compliance" {
3025                self.advance();
3026                node.compliance = self.parse_bracketed_identifiers()?;
3027            }
3028        }
3029
3030        // Optional body: { field: Type, ... }
3031        if self.check(TokenType::LBrace) {
3032            self.advance();
3033            while !self.check(TokenType::RBrace) {
3034                let field_name = self.consume(TokenType::Identifier)?;
3035                let field_loc = self.loc_of(&field_name);
3036                self.consume(TokenType::Colon)?;
3037                let type_expr = self.parse_type_expr()?;
3038                node.fields.push(TypeField {
3039                    name: field_name.value,
3040                    type_expr,
3041                    loc: field_loc,
3042                });
3043                if self.check(TokenType::Comma) {
3044                    self.advance();
3045                }
3046            }
3047            self.consume(TokenType::RBrace)?;
3048        }
3049
3050        Ok(node)
3051    }
3052
3053    fn parse_type_expr(&mut self) -> Result<TypeExpr, ParseError> {
3054        // v2.83.0 — a LEADING bracket is the list-type sugar the README
3055        // has always written in flow signatures: `readings: [SensorReading]`
3056        // (blocks 44-45). It lowers to exactly what `List<SensorReading>`
3057        // produces, so nothing downstream learns a new shape — the v2.0.0
3058        // comment below already names `List<T>` as the canonical carrier.
3059        if self.check(TokenType::LBracket) {
3060            let open = self.current().clone();
3061            self.advance();
3062            let inner = self.parse_type_expr()?;
3063            self.consume(TokenType::RBracket)?;
3064            let mut optional = false;
3065            if self.check(TokenType::Question) {
3066                self.advance();
3067                optional = true;
3068            }
3069            return Ok(TypeExpr {
3070                name: "List".to_string(),
3071                generic_param: if inner.generic_param.is_empty() {
3072                    inner.name
3073                } else {
3074                    format!("{}<{}>", inner.name, inner.generic_param)
3075                },
3076                optional,
3077                loc: self.loc_of(&open),
3078            });
3079        }
3080        let name_tok = self.consume(TokenType::Identifier)?;
3081        let loc = self.loc_of(&name_tok);
3082        let mut generic_param = String::new();
3083        let mut optional = false;
3084
3085        if self.check(TokenType::Lt) {
3086            self.advance();
3087            // v2.0.0 — recursive: the generic param can itself be a
3088            // nested type expression. `FlowEnvelope<List<TenantRecord>>`
3089            // parses as outer=FlowEnvelope, inner=List<TenantRecord>.
3090            // Pre-39.a the inner had to be a single Identifier; nested
3091            // generics like the canonical FlowEnvelope<T> wrapper
3092            // required this lift. Backwards-compat preserved for
3093            // single-level generics like `Stream<Token>` and
3094            // `List<T>` — the recursion lands once and returns the
3095            // same flat string the v1.x parser produced.
3096            let inner = self.parse_type_expr()?;
3097            generic_param = if inner.generic_param.is_empty() {
3098                inner.name
3099            } else {
3100                format!("{}<{}>", inner.name, inner.generic_param)
3101            };
3102            self.consume(TokenType::Gt)?;
3103        }
3104        // v2.4.0 — bracket type parameters for the continuous-carrier
3105        // grammar: `SymbolicPtr[Tensor[Float32]]`, `DensityMatrix[1024]`. The
3106        // param is either a nested type expression OR a numeric dimension.
3107        if self.check(TokenType::LBracket) {
3108            self.advance();
3109            if matches!(self.current().ttype, TokenType::Integer | TokenType::Float) {
3110                generic_param = self.advance().value.clone();
3111            } else {
3112                let inner = self.parse_type_expr()?;
3113                generic_param = if inner.generic_param.is_empty() {
3114                    inner.name
3115                } else {
3116                    format!("{}[{}]", inner.name, inner.generic_param)
3117                };
3118            }
3119            self.consume(TokenType::RBracket)?;
3120        }
3121        if self.check(TokenType::Question) {
3122            self.advance();
3123            optional = true;
3124        }
3125
3126        Ok(TypeExpr {
3127            name: name_tok.value,
3128            generic_param,
3129            optional,
3130            loc,
3131        })
3132    }
3133
3134    /// Parse a type expression in a context where the AST stores the
3135    /// shape as a flat string (step / reason / forge / ots-apply
3136    /// productions). Mirrors Python `_parse_output_type_string`.
3137    ///
3138    /// Accepts:
3139    /// - `Identifier`        → `"Identifier"`
3140    /// - `Stream<String>`    → `"Stream<String>"`
3141    /// - `Optional?`         → `"Optional?"`
3142    /// - `Stream<String>?`   → `"Stream<String>?"`
3143    ///
3144    /// **Why this exists** — pre-fix, the step parser called
3145    /// `consume(TokenType::Identifier)?.value` which captured only
3146    /// the head identifier and left `< … >` unconsumed. For
3147    /// `output: Stream<Token>`, this produced `output_type =
3148    /// "Stream"`, and downstream `flow_has_stream_output`'s
3149    /// `starts_with("Stream<") && ends_with('>')` predicate then
3150    /// returned false → `implicit_transport == "json"` → the
3151    /// dynamic-route fallback in `axon-rs` served JSON instead of
3152    /// SSE even when the adopter's source canonically declared the
3153    /// algebraic stream effect. Surfaced 2026-05-12 by adopter
3154    /// `docs/MIGRATION_TO_AXON.md` audit after the v1.23.0 wire-
3155    /// layer didn't honor the declarative effect. Python parser was
3156    /// fixed for the same gap 2026-05-09; this is the Rust cross-
3157    /// stack catch-up.
3158    fn parse_output_type_string(&mut self) -> Result<String, ParseError> {
3159        let expr = self.parse_type_expr()?;
3160        let mut s = expr.name;
3161        if !expr.generic_param.is_empty() {
3162            s.push('<');
3163            s.push_str(&expr.generic_param);
3164            s.push('>');
3165        }
3166        if expr.optional {
3167            s.push('?');
3168        }
3169        Ok(s)
3170    }
3171
3172    // ── FLOW ─────────────────────────────────────────────────────
3173
3174    fn parse_flow(&mut self) -> Result<FlowDefinition, ParseError> {
3175        let tok = self.consume(TokenType::Flow)?;
3176        let loc = self.loc_of(&tok);
3177        let name = self.consume(TokenType::Identifier)?.value;
3178
3179        self.consume(TokenType::LParen)?;
3180        let mut parameters = Vec::new();
3181        if !self.check(TokenType::RParen) {
3182            parameters = self.parse_param_list()?;
3183        }
3184        self.consume(TokenType::RParen)?;
3185
3186        let mut return_type = None;
3187        if self.check(TokenType::Arrow) {
3188            self.advance();
3189            return_type = Some(self.parse_type_expr()?);
3190        }
3191
3192        self.consume(TokenType::LBrace)?;
3193        let mut body = Vec::new();
3194        while !self.check(TokenType::RBrace) {
3195            body.push(self.parse_flow_step()?);
3196        }
3197        self.consume(TokenType::RBrace)?;
3198
3199        Ok(FlowDefinition {
3200            name,
3201            parameters,
3202            return_type,
3203            body,
3204            loc,
3205            leading_trivia: Vec::new(),
3206            trailing_trivia: Vec::new(),
3207        })
3208    }
3209
3210    // ── v2.87.0 — algebraic effects (Plotkin/Pretnar) ─────────────
3211    //
3212    // Four constructs, in the shape `the design plan` section 3.1 publishes verbatim.
3213
3214    /// `effect SSE { Emit(token: Token) -> Unit  Done() -> Never }`
3215    ///
3216    /// The declaration exists so the operation catalog is CLOSED. the design decision's bare
3217    /// `perform Emit(x)` resolves against exactly this set, and an operation
3218    /// two effects both declare is a compile error naming both — not a silent
3219    /// pick. Without the declaration there would be nothing to resolve against
3220    /// and `effect_name` would be a free string, which is the defect
3221    /// `feedback_free_string_field_breeds_fake_catalog` names.
3222    fn parse_effect(&mut self) -> Result<EffectDefinition, ParseError> {
3223        let tok = self.consume(TokenType::Effect)?;
3224        let loc = self.loc_of(&tok);
3225        let name = self.consume_any_ident_or_kw()?.value;
3226        self.consume(TokenType::LBrace)?;
3227
3228        let mut operations: Vec<EffectOperation> = Vec::new();
3229        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3230            let op_tok = self.current().clone();
3231            let op_name = self.consume_any_ident_or_kw()?.value;
3232
3233            self.consume(TokenType::LParen)?;
3234            let parameters = if self.check(TokenType::RParen) {
3235                Vec::new()
3236            } else {
3237                self.parse_param_list()?
3238            };
3239            self.consume(TokenType::RParen)?;
3240
3241            // `-> T` is optional in the grammar; section 3.1 always writes it, and a
3242            // missing return type reads as Unit at the type-checker.
3243            let mut return_type = String::new();
3244            if self.check(TokenType::Arrow) {
3245                self.advance();
3246                return_type = self.parse_type_expr()?.name;
3247            }
3248
3249            // A duplicate operation name inside ONE effect is refused: the
3250            // handler-clause lookup is by operation name, so two declarations
3251            // would make the arity check depend on which one the search found
3252            // first — a defect nobody would ever see fire.
3253            if let Some(prior) = operations.iter().find(|o| o.name == op_name) {
3254                return Err(ParseError {
3255                    message: format!(
3256                        "effect `{name}` declares operation `{op_name}` twice (first at \
3257                         line {}); handler dispatch is by operation NAME, so a second \
3258                         declaration would silently shadow the first",
3259                        prior.loc.line
3260                    ),
3261                    line: op_tok.line,
3262                    column: op_tok.column,
3263                    ..Default::default()
3264                });
3265            }
3266
3267            operations.push(EffectOperation {
3268                name: op_name,
3269                parameters,
3270                return_type,
3271                loc: self.loc_of(&op_tok),
3272            });
3273        }
3274        self.consume(TokenType::RBrace)?;
3275
3276        Ok(EffectDefinition {
3277            name,
3278            operations,
3279            loc,
3280            leading_trivia: Vec::new(),
3281            trailing_trivia: Vec::new(),
3282        })
3283    }
3284
3285    /// `handle SSE { Emit(token) -> { … } } in { … }` — the delimited handler
3286    /// scope (D3).
3287    ///
3288    /// The `in { … }` body is parsed with [`Self::parse_flow_step`], and that is
3289    /// the whole point of the design decision: the body is ORDINARY flow steps, so
3290    /// `run generate(…)` inside a handler runs for real. Lowering it onto
3291    /// `axon-rs`'s `Instruction` alphabet instead would have made every
3292    /// non-effect node in it a `Passthrough` — inert — which is the v2.67.0 defect
3293    /// this cycle exists not to repeat.
3294    fn parse_handle_block(&mut self) -> Result<HandleBlock, ParseError> {
3295        let tok = self.consume(TokenType::Handle)?;
3296        let loc = self.loc_of(&tok);
3297
3298        // `handle E1, E2 { … }` — one frame may intercept several effects.
3299        let mut effect_names = vec![self.consume_any_ident_or_kw()?.value];
3300        while self.check(TokenType::Comma) {
3301            self.advance();
3302            effect_names.push(self.consume_any_ident_or_kw()?.value);
3303        }
3304
3305        self.consume(TokenType::LBrace)?;
3306        let mut clauses: Vec<HandlerClause> = Vec::new();
3307        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3308            let clause_tok = self.current().clone();
3309            let operation_name = self.consume_any_ident_or_kw()?.value;
3310
3311            // Clause binders are BARE names — `Emit(token) -> { … }`. The types
3312            // live on the effect declaration; repeating them here would let the
3313            // two disagree.
3314            self.consume(TokenType::LParen)?;
3315            let mut parameter_names = Vec::new();
3316            while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
3317                parameter_names.push(self.consume_any_ident_or_kw()?.value);
3318                if self.check(TokenType::Comma) {
3319                    self.advance();
3320                }
3321            }
3322            self.consume(TokenType::RParen)?;
3323            self.consume(TokenType::Arrow)?;
3324            self.consume(TokenType::LBrace)?;
3325
3326            let mut body = Vec::new();
3327            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3328                body.push(self.parse_flow_step()?);
3329            }
3330            self.consume(TokenType::RBrace)?;
3331
3332            if let Some(prior) = clauses.iter().find(|c| c.operation_name == operation_name) {
3333                return Err(ParseError {
3334                    message: format!(
3335                        "handler declares clause `{operation_name}` twice (first at line {}); \
3336                         dispatch finds a clause by operation NAME and would always run the \
3337                         first, leaving the second dead",
3338                        prior.loc.line
3339                    ),
3340                    line: clause_tok.line,
3341                    column: clause_tok.column,
3342                    ..Default::default()
3343                });
3344            }
3345
3346            clauses.push(HandlerClause {
3347                operation_name,
3348                parameter_names,
3349                body,
3350                loc: self.loc_of(&clause_tok),
3351            });
3352        }
3353        self.consume(TokenType::RBrace)?;
3354
3355        // The `in { … }` delimiter is MANDATORY. A `handle` without it declares
3356        // a scope with no extent — nothing could ever be intercepted by it, and
3357        // accepting it would let an author believe an effect was handled when
3358        // no `perform` is inside anything.
3359        let in_tok = self.current().clone();
3360        if !self.check(TokenType::In) {
3361            return Err(ParseError {
3362                message: format!(
3363                    "`handle {}` must be followed by `in {{ … }}` — a handler scope is \
3364                     DELIMITED (the design plan D3). Without the `in` block the frame has no \
3365                     extent, so no `perform` could ever reach these clauses (got '{}')",
3366                    effect_names.join(", "),
3367                    in_tok.value
3368                ),
3369                line: in_tok.line,
3370                column: in_tok.column,
3371                ..Default::default()
3372            });
3373        }
3374        self.advance();
3375        self.consume(TokenType::LBrace)?;
3376        let mut body = Vec::new();
3377        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3378            body.push(self.parse_flow_step()?);
3379        }
3380        self.consume(TokenType::RBrace)?;
3381
3382        Ok(HandleBlock {
3383            effect_names,
3384            clauses,
3385            body,
3386            loc,
3387        })
3388    }
3389
3390    /// The shared head of `perform` and `forward` (D12): an optionally
3391    /// qualified operation name plus a parenthesised argument list.
3392    ///
3393    /// the design decision — BOTH spellings parse. `SSE.Emit(x)` fixes the effect here;
3394    /// `Emit(x)` leaves `effect_name` `None` and the closed catalog resolves it
3395    /// downstream, where an ambiguity can be reported with both candidates
3396    /// named. The qualified form is told from the bare one by the `.`, which
3397    /// cannot appear in an operation name.
3398    fn parse_effect_op_ref(
3399        &mut self,
3400    ) -> Result<(Option<String>, String, Vec<String>), ParseError> {
3401        let first = self.consume_any_ident_or_kw()?.value;
3402        let (effect_name, operation_name) = if self.check(TokenType::Dot) {
3403            self.advance();
3404            (Some(first), self.consume_any_ident_or_kw()?.value)
3405        } else {
3406            (None, first)
3407        };
3408
3409        self.consume(TokenType::LParen)?;
3410        let mut arguments = Vec::new();
3411        while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
3412            // v2.83.0 SUBJECTS: `the design plan` section 3.1 writes `perform
3413            // Emit(response.token)` — a dotted reference into a prior binding.
3414            arguments.push(self.parse_subject()?);
3415            if self.check(TokenType::Comma) {
3416                self.advance();
3417            }
3418        }
3419        self.consume(TokenType::RParen)?;
3420        Ok((effect_name, operation_name, arguments))
3421    }
3422
3423    /// `perform Emit(x)` / `perform SSE.Emit(x)`.
3424    fn parse_perform_step(&mut self) -> Result<PerformStep, ParseError> {
3425        let tok = self.consume(TokenType::Perform)?;
3426        let (effect_name, operation_name, arguments) = self.parse_effect_op_ref()?;
3427        Ok(PerformStep {
3428            effect_name,
3429            operation_name,
3430            arguments,
3431            loc: self.loc_of(&tok),
3432        })
3433    }
3434
3435    /// `forward Emit(t)` / `forward SSE.Emit(t)` (D12).
3436    fn parse_forward_step(&mut self) -> Result<ForwardStep, ParseError> {
3437        let tok = self.consume(TokenType::Forward)?;
3438        let (effect_name, operation_name, arguments) = self.parse_effect_op_ref()?;
3439        Ok(ForwardStep {
3440            effect_name,
3441            operation_name,
3442            arguments,
3443            loc: self.loc_of(&tok),
3444        })
3445    }
3446
3447    /// The shared body of `resume(…)` / `abort(…)`: an optional single value.
3448    fn parse_discharge_value(&mut self) -> Result<String, ParseError> {
3449        self.consume(TokenType::LParen)?;
3450        let value = if self.check(TokenType::RParen) {
3451            String::new()
3452        } else {
3453            self.parse_subject()?
3454        };
3455        self.consume(TokenType::RParen)?;
3456        Ok(value)
3457    }
3458
3459    fn parse_param_list(&mut self) -> Result<Vec<Parameter>, ParseError> {
3460        let mut params = Vec::new();
3461
3462        let name = self.consume(TokenType::Identifier)?;
3463        let ploc = self.loc_of(&name);
3464        self.consume(TokenType::Colon)?;
3465        let type_expr = self.parse_type_expr()?;
3466        params.push(Parameter {
3467            name: name.value,
3468            type_expr,
3469            loc: ploc,
3470        });
3471
3472        while self.check(TokenType::Comma) {
3473            self.advance();
3474            let name = self.consume(TokenType::Identifier)?;
3475            let ploc = self.loc_of(&name);
3476            self.consume(TokenType::Colon)?;
3477            let type_expr = self.parse_type_expr()?;
3478            params.push(Parameter {
3479                name: name.value,
3480                type_expr,
3481                loc: ploc,
3482            });
3483        }
3484        Ok(params)
3485    }
3486
3487    // ── FLOW STEP dispatch ───────────────────────────────────────
3488
3489    fn parse_flow_step(&mut self) -> Result<FlowStep, ParseError> {
3490        let tok = self.current().clone();
3491
3492        match tok.ttype {
3493            // v2.83.0 — an epistemic block INSIDE a flow body. Its
3494            // children are hoisted to program level (see `Parser::hoisted`),
3495            // which is exactly what a top-level block already does, so the
3496            // nested spelling costs nothing downstream. The flow itself gets
3497            // no node: the block declares, it does not execute.
3498            TokenType::Know | TokenType::Believe | TokenType::Speculate
3499                if self
3500                    .tokens
3501                    .get(self.pos + 1)
3502                    .is_some_and(|t| t.ttype == TokenType::LBrace) =>
3503            {
3504                let block = self.parse_epistemic_block()?;
3505                self.hoisted.push(Declaration::Epistemic(block));
3506                self.parse_flow_step()
3507            }
3508            TokenType::Doubt
3509                if self
3510                    .tokens
3511                    .get(self.pos + 1)
3512                    .is_some_and(|t| t.ttype == TokenType::LBrace) =>
3513            {
3514                let block = self.parse_epistemic_block()?;
3515                self.hoisted.push(Declaration::Epistemic(block));
3516                self.parse_flow_step()
3517            }
3518            TokenType::Step => self.parse_step().map(FlowStep::Step),
3519            TokenType::If => self.parse_if().map(FlowStep::If),
3520            TokenType::For => self.parse_for_in().map(FlowStep::ForIn),
3521            TokenType::Let => self.parse_let().map(FlowStep::Let),
3522            TokenType::Return => self.parse_return().map(FlowStep::Return),
3523            TokenType::Break => self.parse_break().map(FlowStep::Break),
3524            TokenType::Continue => self.parse_continue().map(FlowStep::Continue),
3525            TokenType::Lambda => self.parse_lambda_data_apply().map(FlowStep::LambdaDataApply),
3526
3527            // ── Tier 2 flow steps (typed AST) ─────────────────────
3528            TokenType::Probe => self.parse_flow_step_simple("probe").map(|l| FlowStep::Probe(ProbeStep { target: l.1, fields: Vec::new(), loc: l.0 })),
3529            // v2.83.0 — ONE implementation for both positions (the the design decision
3530            // doctrine). `reason <target>` and `reason { given ask depth }` are
3531            // the same node; the second is what the README publishes.
3532            TokenType::Reason => self.parse_reason_step().map(FlowStep::Reason),
3533            TokenType::Validate => self.parse_flow_step_simple("validate").map(|l| FlowStep::Validate(ValidateStep { target: l.1, rule: String::new(), guard: None, loc: l.0 })),
3534            TokenType::Refine => self.parse_flow_step_simple("refine").map(|l| FlowStep::Refine(RefineStep { target: l.1, strategy: String::new(), loc: l.0 })),
3535            TokenType::Weave => self.parse_weave_step(),
3536            TokenType::Use => self.parse_use_step(),
3537            TokenType::Remember => self.parse_remember_step(),
3538            TokenType::Recall => self.parse_recall_step(),
3539            TokenType::Par => self.parse_par_block().map(FlowStep::Par),
3540            TokenType::Hibernate => self.parse_hibernate_step(),
3541            TokenType::Deliberate => self.parse_block_step("deliberate").map(|l| FlowStep::Deliberate(DeliberateBlock { loc: l })),
3542            TokenType::Consensus => self.parse_block_step("consensus").map(|l| FlowStep::Consensus(ConsensusBlock { loc: l })),
3543            TokenType::Forge => self.parse_forge_step().map(FlowStep::Forge),
3544            TokenType::Focus => self.parse_focus_step(),
3545            TokenType::Grad => self.parse_grad_step(),
3546            TokenType::Associate => self.parse_associate_step(),
3547            TokenType::Aggregate => self.parse_aggregate_step(),
3548            TokenType::Explore => self.parse_explore_step(),
3549            TokenType::Ingest => self.parse_ingest_step(),
3550            TokenType::Declassify => self.parse_declassify_step().map(FlowStep::Declassify),
3551            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 })),
3552            // v2.67.0 — `stream` parses its BODY. It used to go through
3553            // `parse_block_step`, whose entire job is `skip_braced_block()` —
3554            // the block's contents were thrown away at parse time, which is why
3555            // `run_stream` had nothing to run and "completed" with an empty
3556            // string while the README sold "Algebraic Effects and Free Monads".
3557            TokenType::Stream => self.parse_stream_block().map(FlowStep::Stream),
3558            // ── v2.87.0 — algebraic effects ────────────────────
3559            //
3560            // All five constructs parse at flow level. `resume` / `abort` /
3561            // `forward` are legal only inside a handler CLAUSE — that scope law
3562            // is enforced by the type-checker (v2.87.0), not here, because the
3563            // parser does not know whether an enclosing `handle` exists when it
3564            // is re-entered through `parse_flow_step` from a clause body.
3565            TokenType::Handle => self.parse_handle_block().map(FlowStep::Handle),
3566            TokenType::Perform => self.parse_perform_step().map(FlowStep::Perform),
3567            TokenType::Resume => {
3568                let tok = self.consume(TokenType::Resume)?;
3569                let value_expr = self.parse_discharge_value()?;
3570                Ok(FlowStep::Resume(ResumeStep {
3571                    value_expr,
3572                    loc: self.loc_of(&tok),
3573                }))
3574            }
3575            TokenType::Abort => {
3576                let tok = self.consume(TokenType::Abort)?;
3577                let value_expr = self.parse_discharge_value()?;
3578                Ok(FlowStep::Abort(AbortStep {
3579                    value_expr,
3580                    loc: self.loc_of(&tok),
3581                }))
3582            }
3583            TokenType::Forward => self.parse_forward_step().map(FlowStep::Forward),
3584            TokenType::Navigate => self.parse_navigate_step(),
3585            TokenType::Drill => self.parse_drill_step(),
3586            TokenType::Trail => self.parse_flow_step_simple("trail").map(|l| FlowStep::Trail(TrailStep { navigate_ref: l.1, loc: l.0 })),
3587            TokenType::Corroborate => self.parse_corroborate_step(),
3588            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 })),
3589            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 })),
3590            // v2.67.0 — `compute <Name> on a, b -> out`. The ARGUMENTS used to
3591            // be `Vec::new()` — hardcoded empty at the parse site — so even if
3592            // the runtime had wanted to compute something, it had nothing to
3593            // compute it FROM.
3594            TokenType::Compute => self.parse_compute_apply().map(FlowStep::ComputeApply),
3595            TokenType::Listen => self.parse_listen_step(),
3596            TokenType::Daemon => self.parse_flow_step_simple("daemon").map(|l| FlowStep::DaemonStep(DaemonStepNode { daemon_ref: l.1, loc: l.0 })),
3597            // v1.6.0 — Mobile typed channels (paper section 3.1, section 3.2, section 4.3)
3598            TokenType::Emit => self.parse_emit_step(),
3599            // v2.46.0 — `mint <Credential> as <binding>` (ephemeral credential).
3600            TokenType::Mint => self.parse_mint_step(),
3601            // v2.48.0 — `rotate <SecretsStore> [where "…"] with <Tool> as
3602            // <binding>` (mediated secret renewal).
3603            TokenType::Rotate => self.parse_rotate_step(),
3604            TokenType::Publish => self.parse_publish_step(),
3605            TokenType::Discover => self.parse_discover_step(),
3606            TokenType::Persist => self.parse_persist_step(),
3607            TokenType::Retrieve => self.parse_retrieve_step(),
3608            TokenType::Mutate => self.parse_mutate_step(),
3609            TokenType::Purge => self.parse_store_where_step().map(|(loc, store_name, where_expr)| FlowStep::Purge(PurgeStep { store_name, where_expr, loc })),
3610            TokenType::Transact => self.parse_block_step("transact").map(|l| FlowStep::Transact(TransactBlock { loc: l })),
3611            // v2.43.0 — the `warden` adversarial-analysis block.
3612            TokenType::Warden => self.parse_warden().map(FlowStep::Warden),
3613            // v2.4.0 — the `quant` cognitive block (Hilbert-space projection).
3614            TokenType::Quant => self.parse_quant().map(FlowStep::Quant),
3615            // v2.4.0 — the `yield` measurement point.
3616            TokenType::Yield => self.parse_yield().map(FlowStep::Yield),
3617            // v2.4.0 — `run <Flow>(args)` as a flow-step: invoke a declared
3618            // flow from inside a body (a `daemon` listen handler, Q3). Reuses
3619            // the top-level run parser.
3620            TokenType::Run => self.parse_run().map(FlowStep::Run),
3621
3622            _ => {
3623                // v1.20.0 — append "Did you mean X?" hint when the
3624                // unknown token looks like a typo'd flow-body keyword
3625                // (e.g. `stepp` / `reasn` / `validte`). D3, D11.
3626                let hint = crate::smart_suggest::suggest_for(
3627                    &tok.value,
3628                    crate::smart_suggest::FLOW_BODY_KEYWORD_NAMES,
3629                );
3630                let base = format!(
3631                    "Unexpected token in flow body: '{}' — expected step, if, for, let, return, ...",
3632                    tok.value
3633                );
3634                let message = if hint.is_empty() {
3635                    base
3636                } else {
3637                    format!("{base}. {hint}")
3638                };
3639                Err(ParseError {
3640                    message,
3641                    line: tok.line,
3642                    column: tok.column,
3643                    ..Default::default()
3644                })
3645            }
3646        }
3647    }
3648
3649    // ── STEP ─────────────────────────────────────────────────────
3650
3651    fn parse_step(&mut self) -> Result<StepNode, ParseError> {
3652        let tok = self.consume(TokenType::Step)?;
3653        let loc = self.loc_of(&tok);
3654        let name = self.consume(TokenType::Identifier)?.value;
3655
3656        let mut persona_ref = String::new();
3657        if self.check(TokenType::Use) {
3658            self.advance();
3659            persona_ref = self.consume_any_ident_or_kw()?.value;
3660        }
3661
3662        self.consume(TokenType::LBrace)?;
3663
3664        let mut node = StepNode {
3665            name,
3666            persona_ref,
3667            given: String::new(),
3668            ask: String::new(),
3669            output_type: String::new(),
3670            confidence_floor: None,
3671            navigate_ref: String::new(),
3672            apply_ref: String::new(),
3673            requires_context: None,
3674            now_tz: None,
3675            guards: Vec::new(),
3676            pix_ops: Vec::new(),
3677            stream: None,
3678            performs: Vec::new(),
3679            loc,
3680        };
3681
3682        self.parse_step_body_into(&mut node)?;
3683        self.consume(TokenType::RBrace)?;
3684        Ok(node)
3685    }
3686
3687    /// v2.83.0 — the step-body field/statement loop, extracted from
3688    /// [`Self::parse_step`] so a `stream<T>` handler arm can reuse it VERBATIM.
3689    ///
3690    /// The caller has already consumed the opening `{` and owns the closing `}`.
3691    ///
3692    /// Extracting it is what keeps `on_chunk: { … }` honest. The published arm
3693    /// body is a STEP body — `probe chunk for […]` followed by
3694    /// `output: QuoteSnapshot` — and `output:` has no flow-level position, so
3695    /// parsing the arm as a flow body would have rejected the README's own
3696    /// example. Re-implementing the loop instead would fork the grammar: every
3697    /// future step-body statement would have to be added twice, and the second
3698    /// copy is the one that rots.
3699    fn parse_step_body_into(&mut self, node: &mut StepNode) -> Result<(), ParseError> {
3700        while !self.check(TokenType::RBrace) {
3701            let inner = self.current().clone();
3702
3703            match inner.ttype {
3704                TokenType::Given => {
3705                    self.advance();
3706                    self.consume(TokenType::Colon)?;
3707                    node.given = self.parse_expression_string()?;
3708                }
3709                TokenType::Ask => {
3710                    self.advance();
3711                    self.consume(TokenType::Colon)?;
3712                    node.ask = self.consume(TokenType::StringLit)?.value;
3713                }
3714                TokenType::Output => {
3715                    // Mirror of Python `_parse_step` `case "output":`
3716                    // which uses `_parse_output_type_string` — accepts
3717                    // the FULL generic-aware shape `Stream<T>`,
3718                    // `Stream<T>?`, `Identifier?`, NOT just the bare
3719                    // head identifier. Pre-fix the step parser dropped
3720                    // `<T>` and downstream `flow_has_stream_output`'s
3721                    // `starts_with("Stream<") && ends_with('>')` then
3722                    // returned false → `implicit_transport == "json"`
3723                    // → dynamic routes served JSON instead of SSE.
3724                    self.advance();
3725                    self.consume(TokenType::Colon)?;
3726                    node.output_type = self.parse_output_type_string()?;
3727                }
3728                // v2.83.0 — `navigate` in a step body is TWO forms, told
3729                // apart by the token after the keyword:
3730                // `navigate: <Ref>` the field (pre-v2.83.0)
3731                //   `navigate <Ref> query: …` the STATEMENT README publishes
3732                // The second is an elevation: it binds `as:` before the step
3733                // generates, so the step's `ask:` can interpolate it.
3734                TokenType::Navigate
3735                    if self
3736                        .tokens
3737                        .get(self.pos + 1)
3738                        .is_some_and(|t| t.ttype != TokenType::Colon) =>
3739                {
3740                    let op = self.parse_navigate_step()?;
3741                    node.pix_ops.push(op);
3742                }
3743                TokenType::Drill => {
3744                    let op = self.parse_drill_step()?;
3745                    node.pix_ops.push(op);
3746                }
3747                TokenType::Trail => {
3748                    let op = self
3749                        .parse_flow_step_simple("trail")
3750                        .map(|l| FlowStep::Trail(TrailStep { navigate_ref: l.1, loc: l.0 }))?;
3751                    node.pix_ops.push(op);
3752                }
3753                // v2.83.0 — `validate <binding> against: <Schema>`, the
3754                // form README's pix family publishes inside a step. The
3755                // flow-level `validate <target>` already exists; this adds the
3756                // step position plus the `against:` clause the docs write.
3757                TokenType::Validate => {
3758                    let tok = self.current().clone();
3759                    self.advance();
3760                    // v2.83.0 — SUBJECT: `validate Assess.output against: X`.
3761                    let target = self.parse_subject()?;
3762                    let mut rule = String::new();
3763                    if self.current().value == "against" {
3764                        self.advance();
3765                        self.consume(TokenType::Colon)?;
3766                        rule = self.consume_any_ident_or_kw()?.value.clone();
3767                    }
3768                    node.pix_ops.push(FlowStep::Validate(ValidateStep {
3769                        target,
3770                        rule,
3771                        guard: None,
3772                        loc: Loc { line: tok.line, column: tok.column },
3773                    }));
3774                }
3775                // v2.88.0 — `if confidence < 0.8 -> refine(max_attempts: 2)`,
3776                // the self-correction guard blocks 1/16/18 publish immediately
3777                // after a `validate … against:`.
3778                //
3779                // Every position in the form is a CLOSED catalog of one — the
3780                // metric (`confidence`), the comparison (`<`), the action
3781                // (`refine`), the argument (`max_attempts`) — and each refusal
3782                // below names its catalog, because a free position here would
3783                // breed the imaginary catalog three cycles have now paid for.
3784                // General branching (`if <cond> { … } else { … }`) stays a
3785                // FLOW-level construct; a step body gets a guard or nothing.
3786                TokenType::If => {
3787                    let tok = self.current().clone();
3788                    self.advance();
3789
3790                    let metric = self.consume_any_ident_or_kw()?;
3791                    if metric.value != "confidence" {
3792                        return Err(ParseError {
3793                            message: format!(
3794                                "step-body `if` is the confidence guard — `if confidence < \
3795                                 <threshold> -> refine(max_attempts: <n>)` — and `confidence` \
3796                                 is its only metric (the CSR the preceding `validate … \
3797                                 against:` computes). Got '{}'. General branching belongs at \
3798                                 flow level: `if <cond> {{ … }}`.",
3799                                metric.value
3800                            ),
3801                            line: metric.line,
3802                            column: metric.column,
3803                            ..Default::default()
3804                        });
3805                    }
3806
3807                    let op = self.current().clone();
3808                    if op.ttype != TokenType::Lt {
3809                        return Err(ParseError {
3810                            message: format!(
3811                                "a confidence guard declares a FLOOR: `if confidence < \
3812                                 <threshold>`. `<` is the only comparison — the guard fires on \
3813                                 DEFICIENCY, and an inverted form would refine the outputs \
3814                                 that already conform. Got '{}'.",
3815                                op.value
3816                            ),
3817                            line: op.line,
3818                            column: op.column,
3819                            ..Default::default()
3820                        });
3821                    }
3822                    self.advance();
3823                    let threshold = self.consume_number()?;
3824
3825                    self.consume(TokenType::Arrow)?;
3826
3827                    if !self.check(TokenType::Refine) {
3828                        let bad = self.current().clone();
3829                        return Err(ParseError {
3830                            message: format!(
3831                                "the guard's action catalog is CLOSED and `refine` is its only \
3832                                 member — the recovery the runtime actually performs (re-derive \
3833                                 the validated value with the violations as feedback, then \
3834                                 re-score). Got '{}'. An action name outside the catalog would \
3835                                 advertise a recovery nothing dispatches.",
3836                                bad.value
3837                            ),
3838                            line: bad.line,
3839                            column: bad.column,
3840                            ..Default::default()
3841                        });
3842                    }
3843                    self.advance();
3844                    self.consume(TokenType::LParen)?;
3845                    let key = self.consume_any_ident_or_kw()?;
3846                    if key.value != "max_attempts" {
3847                        return Err(ParseError {
3848                            message: format!(
3849                                "`refine` takes exactly `max_attempts: <n>` — the bound that \
3850                                 makes the recovery loop TERMINATE by construction. Got '{}'.",
3851                                key.value
3852                            ),
3853                            line: key.line,
3854                            column: key.column,
3855                            ..Default::default()
3856                        });
3857                    }
3858                    self.consume(TokenType::Colon)?;
3859                    let attempts_tok = self.current().clone();
3860                    if attempts_tok.ttype != TokenType::Integer {
3861                        return Err(ParseError {
3862                            message: format!(
3863                                "`max_attempts:` must be a positive integer literal (got '{}')",
3864                                attempts_tok.value
3865                            ),
3866                            line: attempts_tok.line,
3867                            column: attempts_tok.column,
3868                            ..Default::default()
3869                        });
3870                    }
3871                    let max_attempts = attempts_tok.value.parse::<u32>().map_err(|_| ParseError {
3872                        message: format!("Invalid attempt count '{}'", attempts_tok.value),
3873                        line: attempts_tok.line,
3874                        column: attempts_tok.column,
3875                        ..Default::default()
3876                    })?;
3877                    self.advance();
3878                    self.consume(TokenType::RParen)?;
3879
3880                    // ATTACH to the validation this guard governs: the nearest
3881                    // preceding `validate … against:` in THIS step body. The
3882                    // attachment is what makes `confidence` unambiguous by
3883                    // construction — see `ast::ValidateStep::guard`. No such
3884                    // validation ⇒ the guard has nothing to read, and a guard
3885                    // over a score nobody computed is governance theatre.
3886                    let attached = node.pix_ops.iter_mut().rev().find_map(|op| match op {
3887                        FlowStep::Validate(v) if !v.rule.is_empty() => Some(v),
3888                        _ => None,
3889                    });
3890                    match attached {
3891                        Some(v) => {
3892                            if v.guard.is_some() {
3893                                return Err(ParseError {
3894                                    message: "this validation already carries a confidence \
3895                                              guard; a second one would race the first over \
3896                                              the same score. One validation, one floor, one \
3897                                              recovery."
3898                                        .to_string(),
3899                                    line: tok.line,
3900                                    column: tok.column,
3901                                    ..Default::default()
3902                                });
3903                            }
3904                            v.guard = Some(ConfidenceGuard {
3905                                threshold,
3906                                max_attempts,
3907                                loc: Loc { line: tok.line, column: tok.column },
3908                            });
3909                        }
3910                        None => {
3911                            return Err(ParseError {
3912                                message: "`if confidence` reads the CSR of a preceding \
3913                                          `validate … against: <Schema>` in this step body, \
3914                                          and none exists. A `validate` without `against:` \
3915                                          computes no score (there is no schema to score \
3916                                          with), so it cannot carry a guard either."
3917                                    .to_string(),
3918                                line: tok.line,
3919                                column: tok.column,
3920                                ..Default::default()
3921                            });
3922                        }
3923                    }
3924                }
3925                TokenType::Navigate => {
3926                    self.advance();
3927                    self.consume(TokenType::Colon)?;
3928                    node.navigate_ref = self.parse_dotted_identifier()?;
3929                }
3930                TokenType::Identifier if inner.value == "confidence_floor" => {
3931                    self.advance();
3932                    self.consume(TokenType::Colon)?;
3933                    node.confidence_floor = Some(self.consume_number()?);
3934                }
3935                TokenType::Identifier if inner.value == "apply" => {
3936                    self.advance();
3937                    self.consume(TokenType::Colon)?;
3938                    node.apply_ref = self.consume_any_ident_or_kw()?.value;
3939                }
3940                // v2.22.0 — `requires_context: <tokens>`: the step's declared
3941                // model-capability requirement (the context window the cognition
3942                // needs). A bare positive integer literal; the v2.22.0 resolver maps
3943                // it to a concrete model. Range/ceiling is the type-checker's job
3944                // (v2.22.0 positive-int + v2.22.0 catalog ceiling) — the parser only
3945                // requires an integer token here (a float / non-number is a parse
3946                // error, surfaced at the exact column).
3947                TokenType::Identifier if inner.value == "requires_context" => {
3948                    self.advance();
3949                    self.consume(TokenType::Colon)?;
3950                    let num = self.current().clone();
3951                    let bad = |tok: &crate::tokens::Token| ParseError {
3952                        message: format!(
3953                            "`requires_context:` must be a positive integer token count \
3954                             (got '{}')",
3955                            tok.value
3956                        ),
3957                        line: tok.line,
3958                        column: tok.column,
3959                        ..Default::default()
3960                    };
3961                    if num.ttype != TokenType::Integer {
3962                        return Err(bad(&num));
3963                    }
3964                    let value = num.value.parse::<u32>().map_err(|_| bad(&num))?;
3965                    self.advance();
3966                    node.requires_context = Some(value);
3967                }
3968                // v2.46.0 — `now: "<IANA-tz>"`: the step's declared cognitive
3969                // timezone. A string literal; the format law (IANA shape) is the
3970                // type-checker's job (`axon-T892`) — the parser only requires a
3971                // string token here, surfaced at the exact column.
3972                TokenType::Identifier if inner.value == "now" => {
3973                    self.advance();
3974                    self.consume(TokenType::Colon)?;
3975                    let tz = self.current().clone();
3976                    if tz.ttype != TokenType::StringLit {
3977                        return Err(ParseError {
3978                            message: format!(
3979                                "`now:` must be an IANA timezone string literal like \
3980                                 \"America/Bogota\" or \"UTC\" (got '{}')",
3981                                tz.value
3982                            ),
3983                            line: tz.line,
3984                            column: tz.column,
3985                            ..Default::default()
3986                        });
3987                    }
3988                    self.advance();
3989                    node.now_tz = Some(tz.value);
3990                }
3991                // v2.7.0 — a `use` nested inside a `step { }` body used
3992                // to be skipped structurally (grouped with the sub-constructs
3993                // below), silently degrading the tool dispatch to an
3994                // unconstrained LLM step with NO diagnostic. That fallthrough
3995                // drops the AST node before the type-checker can see it, so the
3996                // resource the tool would provision is never linearly accounted
3997                // for (use_tool soundness). Reject it here, at the parser —
3998                // the only place that still sees the token — and redirect to
3999                // the canonical forms.
4000                TokenType::Use => {
4001                    let tool = self
4002                        .tokens
4003                        .get(self.pos + 1)
4004                        .map(|t| t.value.as_str())
4005                        .filter(|v| !v.is_empty())
4006                        .unwrap_or("<Tool>");
4007                    return Err(ParseError {
4008                        message: format!(
4009                            "`use` is not valid inside a `step {{ }}` body — the tool dispatch \
4010                             would be silently dropped. To invoke a tool, either write the \
4011                             flow-level step `use {tool} on <arg>` (outside this block), or bind \
4012                             it inside this step with `apply: {tool}`. To attach a persona, put \
4013                             it in the step header: `step <name> use <Persona> {{ … }}`."
4014                        ),
4015                        line: inner.line,
4016                        column: inner.column,
4017                        ..Default::default()
4018                    });
4019                }
4020                // v2.83.0 — `mandate X on Y`, `shield X on Y -> b`,
4021                // `ots X on Y` as STEP-BODY statements. README XV has always
4022                // written the application here — next to the `output:` it
4023                // constrains — and the parser accepted the same form only at
4024                // flow level, which is why README blocks 40–42 never compiled.
4025                // The published position is also the better semantics: a
4026                // mandate inside a step is scoped to THIS step's generation;
4027                // the flow-level form governs a bare statement whose subject
4028                // must be inferred. One concept, two positions, same AST shape
4029                // as the flow-level `*ApplyStep` family.
4030                TokenType::Mandate => {
4031                    let g = self.parse_step_guard("mandate")?;
4032                    node.guards.push(g);
4033                }
4034                TokenType::Shield => {
4035                    let g = self.parse_step_guard("shield")?;
4036                    node.guards.push(g);
4037                }
4038                TokenType::Ots => {
4039                    let g = self.parse_step_guard("ots")?;
4040                    node.guards.push(g);
4041                }
4042                // v2.83.0 — `lambda RawQuote on ticker -> verified_quote`
4043                // inside a step body: README blocks 46-47's exact shape, the
4044                // the design decision statement position extended to the fourth member of
4045                // the apply family. Semantically it is an ELEVATION, not a
4046                // guard: dispatch runs it BEFORE the step's generation, so the
4047                // elevated binding is in scope for the prompt.
4048                TokenType::Lambda => {
4049                    let g = self.parse_step_guard("lambda")?;
4050                    node.guards.push(g);
4051                }
4052                // v2.83.0 — `probe <target> for [a, b, c]` as a STATEMENT.
4053                //
4054                // `probe` used to fall into `skip_flow_step_structural` below,
4055                // which DISCARDED it — the v2.67.0 silent-drop shape, in the step
4056                // parser. The extraction list had nowhere to live even at flow
4057                // level. Both are fixed here: the statement is kept, and its
4058                // `for [...]` list reaches the AST.
4059                TokenType::Probe
4060                    if self
4061                        .tokens
4062                        .get(self.pos + 1)
4063                        .is_some_and(|t| t.ttype != TokenType::Colon) =>
4064                {
4065                    let tok = self.current().clone();
4066                    self.advance();
4067                    // v2.83.0 — SUBJECT: README psyche writes
4068                    // `probe student.recent_interactions for [...]`.
4069                    let target = self.parse_subject()?;
4070                    let mut fields = Vec::new();
4071                    if self.check(TokenType::For) {
4072                        self.advance();
4073                        self.consume(TokenType::LBracket)?;
4074                        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
4075                            fields.push(self.consume_any_ident_or_kw()?.value.clone());
4076                            if self.check(TokenType::Comma) {
4077                                self.advance();
4078                            }
4079                        }
4080                        self.consume(TokenType::RBracket)?;
4081                    }
4082                    node.pix_ops.push(FlowStep::Probe(ProbeStep {
4083                        target,
4084                        fields,
4085                        loc: Loc { line: tok.line, column: tok.column },
4086                    }));
4087                }
4088                // v2.83.0 — `use_tool <name> [with k: v, …]` as a STATEMENT.
4089                // v2.7.0 made `use` inside a step body a hard error pointing at
4090                // the canonical forms; `use_tool` is the OTHER spelling README
4091                // publishes, and it names the tool explicitly, so there is no
4092                // ambiguity to protect against — the dispatch is not dropped,
4093                // it is recorded.
4094                TokenType::Identifier if inner.value == "use_tool" => {
4095                    let tok = self.current().clone();
4096                    self.advance();
4097                    let tool_name = self.consume_any_ident_or_kw()?.value.clone();
4098                    let args = if self.current().value == "with" {
4099                        self.advance();
4100                        let mut named: Vec<(String, String, String)> = Vec::new();
4101                        loop {
4102                            let k = self.consume_any_ident_or_kw()?.value.clone();
4103                            self.consume(TokenType::Colon)?;
4104                            // `value_kind` mirrors v2.10.0's classification: a
4105                            // string literal is a literal, anything else is a
4106                            // binding reference the runtime must look up.
4107                            let kind = if self.check(TokenType::StringLit) {
4108                                "literal"
4109                            } else {
4110                                "reference"
4111                            };
4112                            let v = self.parse_expression_string()?;
4113                            named.push((k, v, kind.to_string()));
4114                            if self.check(TokenType::Comma) {
4115                                self.advance();
4116                            } else {
4117                                break;
4118                            }
4119                        }
4120                        UseArgs::Named(named)
4121                    } else if self.current().value == "on" {
4122                        self.advance();
4123                        UseArgs::LegacyPositional(
4124                            self.consume_any_ident_or_kw()?.value.clone(),
4125                        )
4126                    } else {
4127                        UseArgs::LegacyPositional(String::new())
4128                    };
4129                    node.pix_ops.push(FlowStep::UseTool(UseToolStep {
4130                        tool_name,
4131                        args,
4132                        loc: Loc { line: tok.line, column: tok.column },
4133                    }));
4134                }
4135                // v2.83.0 — `par { … }` inside a step body.
4136                TokenType::Par => {
4137                    let block = self.parse_par_block()?;
4138                    node.pix_ops.push(FlowStep::Par(block));
4139                }
4140                // v2.83.0 — `reason { given: … ask: "…" depth: N }` as a
4141                // step-body statement. This is the README's single most-published
4142                // cognitive form (16 blocks) and it was the most expensive
4143                // resident of the silent-drop arm below: the block reached
4144                // `skip_flow_step_structural`, which discarded it, so a step
4145                // whose ONLY cognition was a `reason` lowered to an empty `ask`
4146                // and generated over nothing. The elevation position and the
4147                // flow position share `parse_reason_step` — one concept, two
4148                // positions.
4149                // v2.83.0 — `reason` in a step body is TWO forms, told
4150                // apart by the token after the keyword, exactly as v2.83.0 did
4151                // for `navigate`:
4152                //
4153                //   `reason: "…"`             the FIELD — a one-line deliberation
4154                //   `reason { given ask … }`  the STATEMENT README publishes
4155                //
4156                // The field form was already written across this repo's own
4157                // fixtures and it did NOTHING: `skip_flow_step_structural`
4158                // swallowed the key AND its value. Reading it as a `reason`
4159                // whose `ask:` is that value is not new semantics — it is the
4160                // block form with one field, which is what the line says.
4161                TokenType::Reason
4162                    if self
4163                        .tokens
4164                        .get(self.pos + 1)
4165                        .is_some_and(|t| t.ttype == TokenType::Colon) =>
4166                {
4167                    let tok = self.current().clone();
4168                    self.advance();
4169                    self.consume(TokenType::Colon)?;
4170                    let mut r = ReasonStep {
4171                        strategy: String::new(),
4172                        target: String::new(),
4173                        given: String::new(),
4174                        ask: String::new(),
4175                        depth: None,
4176                        loc: self.loc_of(&tok),
4177                    };
4178                    if self.check(TokenType::StringLit) {
4179                        r.ask = self.consume(TokenType::StringLit)?.value;
4180                    } else {
4181                        r.target = self.parse_dotted_identifier()?;
4182                    }
4183                    node.pix_ops.push(FlowStep::Reason(r));
4184                }
4185                TokenType::Reason => {
4186                    let r = self.parse_reason_step()?;
4187                    node.pix_ops.push(FlowStep::Reason(r));
4188                }
4189                // v2.83.0 — `weave [a, b] format: T include: […]` as a
4190                // step-body statement: the shape fourteen README blocks close
4191                // with. It was the worst resident of the silent-drop arm below,
4192                // because it did not merely lose the node — the skipper stops
4193                // at the first `output` KEYWORD it meets, so
4194                // `weave [A.output, B.output]` left the parser mid-list and the
4195                // step then failed with `Expected Colon` pointing at the comma.
4196                // A dropped construct AND a mislocated error.
4197                TokenType::Weave => {
4198                    let w = self.parse_weave_step()?;
4199                    node.pix_ops.push(w);
4200                }
4201                // v2.83.0 — `<Agent>(arg, …)` as a step-body statement:
4202                // the form every agent example in the README uses, and the one
4203                // that makes v2.83.0's executor reachable from source.
4204                //
4205                // Told apart from the field arms above by the `(` — those all
4206                // match on a specific field NAME, so a call can never shadow
4207                // one. The name is a NAME (never dotted: an agent declaration
4208                // has no path), the arguments are v2.83.0 SUBJECTS, because
4209                // README writes `TrendAnalyzer(Gather.output)`.
4210                TokenType::Identifier
4211                    if self
4212                        .tokens
4213                        .get(self.pos + 1)
4214                        .is_some_and(|t| t.ttype == TokenType::LParen) =>
4215                {
4216                    let tok = self.current().clone();
4217                    let agent_name = self.consume_any_ident_or_kw()?.value.clone();
4218                    self.consume(TokenType::LParen)?;
4219                    let mut arguments = Vec::new();
4220                    while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
4221                        arguments.push(self.parse_subject()?);
4222                        if self.check(TokenType::Comma) {
4223                            self.advance();
4224                        }
4225                    }
4226                    self.consume(TokenType::RParen)?;
4227                    node.pix_ops.push(FlowStep::AgentCall(AgentCallStep {
4228                        agent_name,
4229                        arguments,
4230                        loc: self.loc_of(&tok),
4231                    }));
4232                }
4233                // v2.83.0 — `retrieve from <Store> where "…"` as a
4234                // step-body statement. README axonstore writes the store read
4235                // INSIDE the step that consumes it, which is the elevation
4236                // position: the rows must be bound before the step generates.
4237                //
4238                // Unlike the three before it, this one needed no engine work —
4239                // `FlowStep::Retrieve` and `wire_integrations::run_retrieve`
4240                // are among the most-exercised paths in the system (v1.30.0–v1.31.0, the
4241                // pg integration suites). Only the position was missing.
4242                TokenType::Retrieve => {
4243                    let r = self.parse_retrieve_step()?;
4244                    node.pix_ops.push(r);
4245                }
4246                // v2.83.0 — `stream<T> { on_chunk: … on_complete: … }` in a
4247                // step body. THE LAST RESIDENT of the silent-drop arm leaves
4248                // here: `probe` left in v2.83.0, `reason` in v2.83.0, `weave` in
4249                // v2.83.0, `retrieve` in v2.83.0.
4250                //
4251                // What it cost, measured on README block 15 before this landed:
4252                // the whole block — a `probe`, a `validate`, and BOTH `output:`
4253                // declarations — went to `skip_flow_step_structural`, so
4254                // `step Stream` reached the dispatcher with `pix_ops=0`,
4255                // `ask=""`, `output=""`. An entirely EMPTY step, that `axon
4256                // check` passed with 0 errors, and whose `Stream.output` the
4257                // next step then reasoned over. The block had left the v2.81.0
4258                // ledger on the strength of compiling.
4259                //
4260                // NOT a `pix_ops` push — see `StepNode::stream`. The other ten
4261                // statements are elevations that run BEFORE generation; a stream
4262                // handler runs DURING it, and this step's output IS the stream.
4263                TokenType::Stream => {
4264                    let sb = self.parse_stream_block()?;
4265                    if node.stream.is_some() {
4266                        return Err(ParseError {
4267                            message:
4268                                "step declares two `stream` blocks; a step has one output stream, \
4269                                 and composing two has no defined meaning (which one is the \
4270                                 step's output?). Refused rather than silently keeping the last."
4271                                    .to_string(),
4272                            line: inner.line,
4273                            column: inner.column,
4274                            ..Default::default()
4275                        });
4276                    }
4277                    node.stream = Some(Box::new(sb));
4278                }
4279                // v2.87.0 — `perform Op(args)` in a step body, the position
4280                // `the design plan` section 3.1 publishes:
4281                //
4282                //     step generate {
4283                //         given: prompt
4284                //         perform Emit(response.token)
4285                //         perform Done()
4286                //     }
4287                //
4288                // NOT a `pix_ops` push, and this is the v2.83.0 lesson applied a
4289                // second time. Every `pix_ops` statement is an ELEVATION that
4290                // runs BEFORE the step generates. The performed ARGUMENT here is
4291                // the step's own output, so running it as an elevation would
4292                // hand the handler an unresolved symbol and put a NAME on the
4293                // wire where the adopter expected a token — a defect that shows
4294                // up as garbage output, never as an error.
4295                TokenType::Perform => {
4296                    let p = self.parse_perform_step()?;
4297                    node.performs.push(p);
4298                }
4299                // Sub-construct (probe, non-statement form) → skip structurally.
4300                // The REAL `probe … for […]` statement is taken by the guarded
4301                // arm above; this catches only the bare legacy shape.
4302                TokenType::Probe => {
4303                    self.skip_flow_step_structural()?;
4304                }
4305                _ => {
4306                    return Err(ParseError {
4307                        message: format!(
4308                            "Unexpected token in step body: '{}' — expected given, ask, \
4309                             probe, reason, weave, stream, perform, output, confidence_floor, \
4310                             navigate, apply, requires_context, now",
4311                            inner.value
4312                        ),
4313                        line: inner.line,
4314                        column: inner.column,
4315                                            ..Default::default()
4316                    });
4317                }
4318            }
4319        }
4320        Ok(())
4321    }
4322
4323    /// Skip a flow-level sub-construct structurally (consume keyword + args + optional block).
4324    fn skip_flow_step_structural(&mut self) -> Result<(), ParseError> {
4325        // Consume the keyword
4326        self.advance();
4327        // Consume tokens until we hit a { or a closing }, or a known flow step keyword
4328        while !self.check(TokenType::LBrace)
4329            && !self.check(TokenType::RBrace)
4330            && !self.check(TokenType::Eof)
4331        {
4332            // Check if we hit a new step-level keyword (means this was a one-liner)
4333            let tt = &self.current().ttype;
4334            if matches!(
4335                tt,
4336                TokenType::Step
4337                    | TokenType::Given
4338                    | TokenType::Ask
4339                    | TokenType::Output
4340                    | TokenType::Navigate
4341                    | TokenType::Use
4342                    | TokenType::Probe
4343                    | TokenType::Reason
4344                    | TokenType::Weave
4345                    | TokenType::Stream
4346                    | TokenType::If
4347                    | TokenType::For
4348                    | TokenType::Let
4349                    | TokenType::Return
4350            ) {
4351                return Ok(());
4352            }
4353            self.advance();
4354        }
4355        // If block, skip it
4356        if self.check(TokenType::LBrace) {
4357            self.skip_braced_block()?;
4358        }
4359        Ok(())
4360    }
4361
4362    // ── INTENT ───────────────────────────────────────────────────
4363
4364    fn parse_intent(&mut self) -> Result<IntentNode, ParseError> {
4365        let tok = self.consume(TokenType::Intent)?;
4366        let loc = self.loc_of(&tok);
4367        let name = self.consume(TokenType::Identifier)?.value;
4368        self.consume(TokenType::LBrace)?;
4369
4370        let mut node = IntentNode {
4371            name,
4372            given: String::new(),
4373            ask: String::new(),
4374            output_type: None,
4375            confidence_floor: None,
4376            loc,
4377            leading_trivia: Vec::new(),
4378            trailing_trivia: Vec::new(),
4379        };
4380
4381        while !self.check(TokenType::RBrace) {
4382            let field_name = self.current().value.clone();
4383            self.advance();
4384            self.consume(TokenType::Colon)?;
4385
4386            match field_name.as_str() {
4387                "given" => node.given = self.consume(TokenType::Identifier)?.value,
4388                "ask" => node.ask = self.consume(TokenType::StringLit)?.value,
4389                "output" => node.output_type = Some(self.parse_type_expr()?),
4390                "confidence_floor" => node.confidence_floor = Some(self.consume_number()?),
4391                _ => self.skip_value(),
4392            }
4393        }
4394        self.consume(TokenType::RBrace)?;
4395        Ok(node)
4396    }
4397
4398    // ── RUN ──────────────────────────────────────────────────────
4399
4400    fn parse_run(&mut self) -> Result<RunStatement, ParseError> {
4401        let tok = self.consume(TokenType::Run)?;
4402        let loc = self.loc_of(&tok);
4403        let flow_name = self.consume(TokenType::Identifier)?.value;
4404
4405        self.consume(TokenType::LParen)?;
4406        let mut arguments = Vec::new();
4407        if !self.check(TokenType::RParen) {
4408            arguments = self.parse_argument_list()?;
4409        }
4410        self.consume(TokenType::RParen)?;
4411
4412        let mut node = RunStatement {
4413            flow_name,
4414            arguments,
4415            persona: String::new(),
4416            context: String::new(),
4417            anchors: Vec::new(),
4418            on_failure: String::new(),
4419            on_failure_params: Vec::new(),
4420            output_to: String::new(),
4421            effort: String::new(),
4422            loc,
4423            leading_trivia: Vec::new(),
4424            trailing_trivia: Vec::new(),
4425        };
4426
4427        while self.check_run_modifier() {
4428            let mod_tok = self.current().clone();
4429            // v2.83.0 — `with <Persona>`, README's spelling of `as`.
4430            if mod_tok.value == "with" && mod_tok.ttype != TokenType::As {
4431                self.advance();
4432                node.persona = self.consume(TokenType::Identifier)?.value;
4433                continue;
4434            }
4435            match mod_tok.ttype {
4436                TokenType::As => {
4437                    self.advance();
4438                    node.persona = self.consume(TokenType::Identifier)?.value;
4439                }
4440                TokenType::Within => {
4441                    self.advance();
4442                    node.context = self.consume(TokenType::Identifier)?.value;
4443                }
4444                TokenType::ConstrainedBy => {
4445                    self.advance();
4446                    node.anchors = self.parse_bracketed_identifiers()?;
4447                }
4448                TokenType::OnFailure => {
4449                    self.advance();
4450                    self.consume(TokenType::Colon)?;
4451                    node.on_failure = self.consume_any_ident_or_kw()?.value;
4452                    // Parse optional params: (key: val, ...)
4453                    if self.check(TokenType::LParen) {
4454                        self.advance();
4455                        while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
4456                            let key = self.consume_any_ident_or_kw()?.value;
4457                            self.consume(TokenType::Colon)?;
4458                            let val = self.consume_any_ident_or_kw()?.value;
4459                            node.on_failure_params.push((key, val));
4460                            if self.check(TokenType::Comma) {
4461                                self.advance();
4462                            }
4463                        }
4464                        if self.check(TokenType::RParen) {
4465                            self.advance();
4466                        }
4467                    }
4468                }
4469                TokenType::OutputTo => {
4470                    self.advance();
4471                    self.consume(TokenType::Colon)?;
4472                    node.output_to = self.consume(TokenType::StringLit)?.value;
4473                }
4474                TokenType::Effort => {
4475                    self.advance();
4476                    self.consume(TokenType::Colon)?;
4477                    node.effort = self.consume_any_ident_or_kw()?.value;
4478                }
4479                _ => break,
4480            }
4481        }
4482
4483        Ok(node)
4484    }
4485
4486    // ── EPISTEMIC BLOCK ──────────────────────────────────────────
4487
4488    fn parse_epistemic_block(&mut self) -> Result<EpistemicBlock, ParseError> {
4489        let tok = self.current().clone();
4490        let mode = match tok.ttype {
4491            TokenType::Know => "know",
4492            TokenType::Believe => "believe",
4493            TokenType::Speculate => "speculate",
4494            TokenType::Doubt => "doubt",
4495            _ => unreachable!(),
4496        };
4497        self.advance();
4498        let loc = self.loc_of(&tok);
4499
4500        self.consume(TokenType::LBrace)?;
4501        let mut body = Vec::new();
4502        while !self.check(TokenType::RBrace) {
4503            body.push(self.parse_declaration()?);
4504        }
4505        self.consume(TokenType::RBrace)?;
4506
4507        Ok(EpistemicBlock {
4508            mode: mode.to_string(),
4509            body,
4510            loc,
4511            leading_trivia: Vec::new(),
4512            trailing_trivia: Vec::new(),
4513        })
4514    }
4515
4516    // ── IF ────────────────────────────────────────────────────────
4517
4518    // ── v2.26.0 — the pure expression engine (Pratt parser) ───────────
4519
4520    /// Parse a pure expression (v2.26.0). Precedence-climbing: `or` < `and` <
4521    /// comparison < `+ -` < `* / %` < unary (`- not`) < atom. Total + pure; no
4522    /// side effects. Field/index access + the builtin catalog land in v2.26.0.
4523    fn parse_expr(&mut self) -> Result<Expr, ParseError> {
4524        self.parse_expr_bp(0)
4525    }
4526
4527    fn parse_expr_bp(&mut self, min_bp: u8) -> Result<Expr, ParseError> {
4528        // Prefix: unary `-` (negation) / `not` (boolean). Binds tighter than
4529        // every binary operator (bp 6).
4530        let mut lhs = match self.current().ttype {
4531            TokenType::Minus => {
4532                self.advance();
4533                Expr::Unary(UnOp::Neg, Box::new(self.parse_expr_bp(6)?))
4534            }
4535            TokenType::Not => {
4536                self.advance();
4537                Expr::Unary(UnOp::Not, Box::new(self.parse_expr_bp(6)?))
4538            }
4539            _ => self.parse_postfix()?,
4540        };
4541        // Infix: left-associative (right_bp = left_bp + 1).
4542        while let Some((op, lbp)) = Self::binop_of(self.current().ttype.clone()) {
4543            if lbp < min_bp {
4544                break;
4545            }
4546            self.advance();
4547            let rhs = self.parse_expr_bp(lbp + 1)?;
4548            lhs = Expr::Binary(op, Box::new(lhs), Box::new(rhs));
4549        }
4550        Ok(lhs)
4551    }
4552
4553    /// Map a token to `(BinOp, left binding power)`, or `None` if it is not an
4554    /// infix operator (which stops the climb — e.g. at `->` or `{`).
4555    fn binop_of(t: TokenType) -> Option<(BinOp, u8)> {
4556        Some(match t {
4557            TokenType::Or => (BinOp::Or, 1),
4558            TokenType::And => (BinOp::And, 2),
4559            TokenType::Eq => (BinOp::Eq, 3),
4560            TokenType::Neq => (BinOp::Ne, 3),
4561            TokenType::Lt => (BinOp::Lt, 3),
4562            TokenType::Lte => (BinOp::Le, 3),
4563            TokenType::Gt => (BinOp::Gt, 3),
4564            TokenType::Gte => (BinOp::Ge, 3),
4565            TokenType::Plus => (BinOp::Add, 4),
4566            TokenType::Minus => (BinOp::Sub, 4),
4567            TokenType::Star => (BinOp::Mul, 5),
4568            TokenType::Slash => (BinOp::Div, 5),
4569            TokenType::Percent => (BinOp::Mod, 5),
4570            _ => return None,
4571        })
4572    }
4573
4574    /// v2.26.0 — parse a primary then its `.` postfix chain: a builtin call
4575    /// (`.length`, `.contains(x)`) when the name is in the closed catalog, else
4576    /// a dotted reference-path continuation (`a.b.c` → `Ref("a.b.c")`, the
4577    /// pre-v2.26.0 behaviour). Field access on a non-reference (`(a+b).x`) is
4578    /// reserved for v2.26.0.
4579    fn parse_postfix(&mut self) -> Result<Expr, ParseError> {
4580        let mut expr = self.parse_expr_atom()?;
4581        loop {
4582            if self.check(TokenType::Dot) {
4583                self.advance();
4584                let name = self.consume_any_ident_or_kw()?.value;
4585                if let Some(builtin) = Builtin::from_name(&name) {
4586                    let mut args = vec![expr];
4587                    if self.check(TokenType::LParen) {
4588                        self.advance();
4589                        while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
4590                            args.push(self.parse_expr_bp(0)?);
4591                            if self.check(TokenType::Comma) {
4592                                self.advance();
4593                            } else {
4594                                break;
4595                            }
4596                        }
4597                        self.consume(TokenType::RParen)?;
4598                    }
4599                    expr = Expr::Call(builtin, args);
4600                } else {
4601                    // v2.26.0 — a plain dotted path on a Ref extends the Ref
4602                    // (back-compat: `a.b.c` → `Ref("a.b.c")`); on any other base
4603                    // it is a structured field access (the JSONB seam).
4604                    expr = match expr {
4605                        Expr::Ref(p) => Expr::Ref(format!("{p}.{name}")),
4606                        other => Expr::Field(Box::new(other), name),
4607                    };
4608                }
4609            } else if self.check(TokenType::LBracket) {
4610                // v2.26.0 — index access `base[index]`.
4611                self.advance();
4612                let index = self.parse_expr_bp(0)?;
4613                self.consume(TokenType::RBracket)?;
4614                expr = Expr::Index(Box::new(expr), Box::new(index));
4615            } else {
4616                break;
4617            }
4618        }
4619        Ok(expr)
4620    }
4621
4622    fn parse_expr_atom(&mut self) -> Result<Expr, ParseError> {
4623        let tok = self.current().clone();
4624        match tok.ttype {
4625            TokenType::Integer => {
4626                self.advance();
4627                let lit = tok
4628                    .value
4629                    .parse::<i64>()
4630                    .map(ExprLit::Int)
4631                    .or_else(|_| tok.value.parse::<f64>().map(ExprLit::Float))
4632                    .map_err(|_| ParseError {
4633                        message: format!("invalid integer literal '{}'", tok.value),
4634                        line: tok.line,
4635                        column: tok.column,
4636                        ..Default::default()
4637                    })?;
4638                Ok(Expr::Lit(lit))
4639            }
4640            TokenType::Float => {
4641                self.advance();
4642                let f = tok.value.parse::<f64>().map_err(|_| ParseError {
4643                    message: format!("invalid float literal '{}'", tok.value),
4644                    line: tok.line,
4645                    column: tok.column,
4646                    ..Default::default()
4647                })?;
4648                Ok(Expr::Lit(ExprLit::Float(f)))
4649            }
4650            TokenType::Bool => {
4651                self.advance();
4652                Ok(Expr::Lit(ExprLit::Bool(tok.value == "true")))
4653            }
4654            TokenType::StringLit => {
4655                self.advance();
4656                Ok(Expr::Lit(ExprLit::Str(tok.value)))
4657            }
4658            TokenType::LParen => {
4659                self.advance();
4660                let inner = self.parse_expr_bp(0)?;
4661                self.consume(TokenType::RParen)?;
4662                Ok(inner)
4663            }
4664            _ => {
4665                // Reference: a single identifier (or keyword used as a name).
4666                // The `.` chain (dotted path / builtin call) is handled by the
4667                // postfix layer (v2.26.0 `parse_postfix`).
4668                Ok(Expr::Ref(self.consume_any_ident_or_kw()?.value))
4669            }
4670        }
4671    }
4672
4673    /// v2.26.0 — render a literal to its legacy surface string (for the
4674    /// back-compat `(condition, op, value)` triple). Only used when an
4675    /// expression fits the legacy shape; numeric round-tripping is exact for
4676    /// ints and faithful-enough for floats (the legacy runtime re-parses it).
4677    fn expr_lit_surface(lit: &ExprLit) -> String {
4678        match lit {
4679            ExprLit::Int(i) => i.to_string(),
4680            ExprLit::Float(f) => f.to_string(),
4681            ExprLit::Bool(b) => b.to_string(),
4682            ExprLit::Str(s) => s.clone(),
4683        }
4684    }
4685
4686    fn expr_leaf_surface(expr: &Expr) -> Option<String> {
4687        match expr {
4688            Expr::Ref(p) => Some(p.clone()),
4689            Expr::Lit(l) => Some(Self::expr_lit_surface(l)),
4690            _ => None,
4691        }
4692    }
4693
4694    /// A legacy "leaf" is a bare reference (truthy check) or a
4695    /// `<ref> <cmp> <ref|literal>` triple — exactly what the pre-v2.26.0 `if`
4696    /// grammar could express.
4697    fn expr_legacy_leaf(expr: &Expr) -> Option<(String, String, String)> {
4698        match expr {
4699            Expr::Ref(p) => Some((p.clone(), String::new(), String::new())),
4700            Expr::Binary(op, l, r) => {
4701                let op_s = match op {
4702                    BinOp::Eq => "==",
4703                    BinOp::Ne => "!=",
4704                    BinOp::Lt => "<",
4705                    BinOp::Le => "<=",
4706                    BinOp::Gt => ">",
4707                    BinOp::Ge => ">=",
4708                    _ => return None,
4709                };
4710                let lhs = match &**l {
4711                    Expr::Ref(p) => p.clone(),
4712                    _ => return None,
4713                };
4714                let rhs = Self::expr_leaf_surface(r)?;
4715                Some((lhs, op_s.to_string(), rhs))
4716            }
4717            _ => None,
4718        }
4719    }
4720
4721    /// Flatten an `or`-tree of legacy leaves in left-to-right order. Returns
4722    /// `false` (and leaves `out` unusable) if any node is not a legacy leaf.
4723    fn collect_or_leaves(expr: &Expr, out: &mut Vec<(String, String, String)>) -> bool {
4724        match expr {
4725            Expr::Binary(BinOp::Or, l, r) => {
4726                Self::collect_or_leaves(l, out) && Self::collect_or_leaves(r, out)
4727            }
4728            _ => match Self::expr_legacy_leaf(expr) {
4729                Some(t) => {
4730                    out.push(t);
4731                    true
4732                }
4733                None => false,
4734            },
4735        }
4736    }
4737
4738    /// v2.26.0 — if the parsed condition fits the legacy
4739    /// `(condition, op, value)` + `or`-chain shape, return the legacy fields so
4740    /// the IR + runtime stay byte-identical to pre-v2.26.0 (zero drift). `None` ⇒
4741    /// the condition uses richer forms (`and`, `not`, arithmetic, parentheses,
4742    /// nesting) and must ride the `cond` expression evaluator.
4743    #[allow(clippy::type_complexity)]
4744    fn cond_as_legacy(
4745        expr: &Expr,
4746    ) -> Option<(String, String, String, Vec<(String, String, String)>, String)> {
4747        let mut leaves = Vec::new();
4748        if !Self::collect_or_leaves(expr, &mut leaves) || leaves.is_empty() {
4749            return None;
4750        }
4751        let (c0, o0, v0) = leaves[0].clone();
4752        let rest = leaves[1..].to_vec();
4753        let conjunctor = if rest.is_empty() {
4754            String::new()
4755        } else {
4756            "or".to_string()
4757        };
4758        Some((c0, o0, v0, rest, conjunctor))
4759    }
4760
4761    fn parse_if(&mut self) -> Result<ConditionalNode, ParseError> {
4762        let tok = self.consume(TokenType::If)?;
4763        let loc = self.loc_of(&tok);
4764
4765        // v2.26.0 — parse the condition as a pure expression, then split:
4766        // a legacy-expressible condition populates the legacy triple fields
4767        // (cond = None → byte-identical IR + eval); a richer condition rides
4768        // the `cond` expression evaluator.
4769        let expr = self.parse_expr()?;
4770        let (condition, comparison_op, comparison_value, conditions, conjunctor, cond) =
4771            match Self::cond_as_legacy(&expr) {
4772                Some((c, o, v, more, conj)) => (c, o, v, more, conj, None),
4773                None => (
4774                    String::new(),
4775                    String::new(),
4776                    String::new(),
4777                    Vec::new(),
4778                    String::new(),
4779                    Some(expr),
4780                ),
4781            };
4782
4783        let mut then_body = Vec::new();
4784        let mut else_body = Vec::new();
4785
4786        // Arrow form or block form
4787        if self.check(TokenType::Arrow) {
4788            self.advance();
4789            then_body.push(self.parse_flow_step()?);
4790        } else if self.check(TokenType::LBrace) {
4791            self.advance();
4792            while !self.check(TokenType::RBrace) {
4793                then_body.push(self.parse_flow_step()?);
4794            }
4795            self.consume(TokenType::RBrace)?;
4796        }
4797
4798        // Else branch
4799        if self.check(TokenType::Else) {
4800            self.advance();
4801            if self.check(TokenType::Arrow) {
4802                self.advance();
4803                else_body.push(self.parse_flow_step()?);
4804            } else if self.check(TokenType::LBrace) {
4805                self.advance();
4806                while !self.check(TokenType::RBrace) {
4807                    else_body.push(self.parse_flow_step()?);
4808                }
4809                self.consume(TokenType::RBrace)?;
4810            }
4811        }
4812
4813        Ok(ConditionalNode {
4814            condition,
4815            comparison_op,
4816            comparison_value,
4817            then_body,
4818            else_body,
4819            conditions,
4820            conjunctor,
4821            cond,
4822            loc,
4823        })
4824    }
4825
4826    // ── FOR IN ───────────────────────────────────────────────────
4827
4828    fn parse_for_in(&mut self) -> Result<ForInStatement, ParseError> {
4829        let tok = self.consume(TokenType::For)?;
4830        let loc = self.loc_of(&tok);
4831        let variable = self.consume(TokenType::Identifier)?.value;
4832        self.consume(TokenType::In)?;
4833        let iterable = self.parse_dotted_identifier()?;
4834
4835        self.consume(TokenType::LBrace)?;
4836        // v1.14.0 — increment loop_depth so `parse_break` /
4837        // `parse_continue` inside the body pass the scope check.
4838        // Decrement on every exit path (Ok / Err) so a parse error
4839        // mid-body does not leave the depth permanently elevated
4840        // for later top-level parsing — `?` would skip the
4841        // decrement otherwise.
4842        self.loop_depth += 1;
4843        let body_result = (|| -> Result<Vec<FlowStep>, ParseError> {
4844            let mut body = Vec::new();
4845            while !self.check(TokenType::RBrace) {
4846                body.push(self.parse_flow_step()?);
4847            }
4848            Ok(body)
4849        })();
4850        self.loop_depth -= 1;
4851        let body = body_result?;
4852        self.consume(TokenType::RBrace)?;
4853
4854        Ok(ForInStatement {
4855            variable,
4856            iterable,
4857            body,
4858            loc,
4859        })
4860    }
4861
4862    /// v1.14.0 — `break` keyword. Compile-time scope check
4863    /// (`loop_depth == 0`) rejects break outside a for-in body.
4864    fn parse_break(&mut self) -> Result<BreakStatement, ParseError> {
4865        let tok = self.consume(TokenType::Break)?;
4866        let loc = self.loc_of(&tok);
4867        if self.loop_depth == 0 {
4868            return Err(ParseError {
4869                message: "'break' outside of a for-in loop body".to_string(),
4870                line: tok.line,
4871                column: tok.column,
4872                            ..Default::default()
4873            });
4874        }
4875        Ok(BreakStatement { loc })
4876    }
4877
4878    /// v1.14.0 — `continue` keyword. Same scope check as
4879    /// `parse_break`.
4880    fn parse_continue(&mut self) -> Result<ContinueStatement, ParseError> {
4881        let tok = self.consume(TokenType::Continue)?;
4882        let loc = self.loc_of(&tok);
4883        if self.loop_depth == 0 {
4884            return Err(ParseError {
4885                message: "'continue' outside of a for-in loop body".to_string(),
4886                line: tok.line,
4887                column: tok.column,
4888                            ..Default::default()
4889            });
4890        }
4891        Ok(ContinueStatement { loc })
4892    }
4893
4894    // ── LET ──────────────────────────────────────────────────────
4895
4896    fn parse_let(&mut self) -> Result<LetStatement, ParseError> {
4897        let tok = self.consume(TokenType::Let)?;
4898        let loc = self.loc_of(&tok);
4899
4900        // Name can be an identifier or a keyword used as binding name
4901        let name = self.consume_any_ident_or_kw()?.value;
4902        // v2.4.0 — optional type annotation `let x: <TypeExpr> = …`.
4903        let type_annotation = if self.check(TokenType::Colon) {
4904            self.advance();
4905            Some(self.parse_type_expr()?)
4906        } else {
4907            None
4908        };
4909        self.consume(TokenType::Assign)?;
4910        // v1.12.0 — reset side-channel before parsing value; the
4911        // atom / expr helpers tag the kind as they descend.
4912        self.last_let_value_kind = "literal".to_string();
4913        let (value, value_ast) = self.parse_let_value_expr_with_ast()?;
4914
4915        Ok(LetStatement {
4916            identifier: name,
4917            value_expr: value,
4918            value_kind: self.last_let_value_kind.clone(),
4919            type_annotation,
4920            value_ast,
4921            loc,
4922            leading_trivia: Vec::new(),
4923            trailing_trivia: Vec::new(),
4924        })
4925    }
4926
4927    fn parse_let_value_expr(&mut self) -> Result<String, ParseError> {
4928        let atom = self.parse_let_atom()?;
4929
4930        // Arithmetic expression: collect as string
4931        if matches!(
4932            self.current().ttype,
4933            TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
4934        ) {
4935            let mut parts = vec![atom];
4936            while matches!(
4937                self.current().ttype,
4938                TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
4939            ) {
4940                parts.push(self.advance().value.clone());
4941                parts.push(self.parse_let_atom()?);
4942            }
4943            self.last_let_value_kind = "expression".to_string();
4944            return Ok(parts.join(" "));
4945        }
4946        Ok(atom)
4947    }
4948
4949    /// v2.26.0 — parse a `let`-binding value, additionally producing a
4950    /// structured `value_ast` for the expression case. A list literal keeps the
4951    /// dedicated path; everything else is parsed through the v2.26.0 expression
4952    /// engine and classified: a bare literal / reference keeps its pre-v2.26.0
4953    /// string form (`value_ast = None`, byte-identical), while a real expression
4954    /// (`price * qty`, `recent.length`) additionally carries a `value_ast` the
4955    /// runtime evaluates for real (pre-v2.26.0 it was treated as an opaque literal
4956    /// string). Used ONLY by `parse_let` — other value positions (list items,
4957    /// remember/stream values) keep the string-only `parse_let_value_expr`.
4958    fn parse_let_value_expr_with_ast(&mut self) -> Result<(String, Option<Expr>), ParseError> {
4959        if self.check(TokenType::LBracket) {
4960            self.last_let_value_kind = "literal".to_string();
4961            return Ok((self.parse_let_list_literal()?, None));
4962        }
4963        let expr = self.parse_expr()?;
4964        Ok(match expr {
4965            Expr::Lit(lit) => {
4966                self.last_let_value_kind = "literal".to_string();
4967                (Self::expr_lit_surface(&lit), None)
4968            }
4969            Expr::Ref(p) => {
4970                self.last_let_value_kind = "reference".to_string();
4971                (p, None)
4972            }
4973            other => {
4974                self.last_let_value_kind = "expression".to_string();
4975                (Self::render_expr(&other), Some(other))
4976            }
4977        })
4978    }
4979
4980    /// v2.26.0 — a readable surface rendering of an expression for the
4981    /// vestigial `value_expr` string (the runtime uses `value_ast`).
4982    fn render_expr(e: &Expr) -> String {
4983        match e {
4984            Expr::Lit(l) => Self::expr_lit_surface(l),
4985            Expr::Ref(p) => p.clone(),
4986            // v2.83.0 — surface form of a `logic { }` chain. This string is
4987            // vestigial (the runtime evaluates `value_ast`), so it renders the
4988            // shape rather than trying to reconstruct the author's layout.
4989            Expr::Let { name, value, body } => format!(
4990                "let {name} = {} in {}",
4991                Self::render_expr(value),
4992                Self::render_expr(body)
4993            ),
4994            Expr::Unary(UnOp::Neg, x) => format!("-{}", Self::render_expr(x)),
4995            Expr::Unary(UnOp::Not, x) => format!("not {}", Self::render_expr(x)),
4996            Expr::Binary(op, l, r) => {
4997                let sym = match op {
4998                    BinOp::Add => "+",
4999                    BinOp::Sub => "-",
5000                    BinOp::Mul => "*",
5001                    BinOp::Div => "/",
5002                    BinOp::Mod => "%",
5003                    BinOp::Eq => "==",
5004                    BinOp::Ne => "!=",
5005                    BinOp::Lt => "<",
5006                    BinOp::Le => "<=",
5007                    BinOp::Gt => ">",
5008                    BinOp::Ge => ">=",
5009                    BinOp::And => "and",
5010                    BinOp::Or => "or",
5011                };
5012                format!("({} {sym} {})", Self::render_expr(l), Self::render_expr(r))
5013            }
5014            Expr::Call(b, args) => {
5015                let recv = args.first().map(Self::render_expr).unwrap_or_default();
5016                let rest: Vec<String> = args.iter().skip(1).map(Self::render_expr).collect();
5017                if rest.is_empty() {
5018                    format!("{recv}.{}", b.surface())
5019                } else {
5020                    format!("{recv}.{}({})", b.surface(), rest.join(", "))
5021                }
5022            }
5023            Expr::Field(b, f) => format!("{}.{f}", Self::render_expr(b)),
5024            Expr::Index(b, i) => format!("{}[{}]", Self::render_expr(b), Self::render_expr(i)),
5025        }
5026    }
5027
5028    fn parse_let_atom(&mut self) -> Result<String, ParseError> {
5029        let tok = self.current().clone();
5030
5031        match tok.ttype {
5032            TokenType::StringLit => {
5033                self.last_let_value_kind = "literal".to_string();
5034                self.advance();
5035                Ok(tok.value)
5036            }
5037            TokenType::Integer | TokenType::Float => {
5038                self.last_let_value_kind = "literal".to_string();
5039                self.advance();
5040                Ok(tok.value)
5041            }
5042            TokenType::Bool => {
5043                self.last_let_value_kind = "literal".to_string();
5044                self.advance();
5045                Ok(tok.value)
5046            }
5047            TokenType::Identifier => {
5048                self.last_let_value_kind = "reference".to_string();
5049                self.parse_dotted_identifier()
5050            }
5051            TokenType::LBracket => {
5052                self.last_let_value_kind = "literal".to_string();
5053                self.parse_let_list_literal()
5054            }
5055            _ => {
5056                // Keywords starting a dotted path (pix.document_tree)
5057                if self.pos + 1 < self.tokens.len()
5058                    && self.tokens[self.pos + 1].ttype == TokenType::Dot
5059                {
5060                    self.last_let_value_kind = "reference".to_string();
5061                    return self.parse_dotted_identifier();
5062                }
5063                Err(ParseError {
5064                    message: format!(
5065                        "Expected value expression, found {:?}('{}')",
5066                        tok.ttype, tok.value
5067                    ),
5068                    line: tok.line,
5069                    column: tok.column,
5070                                    ..Default::default()
5071                })
5072            }
5073        }
5074    }
5075
5076    fn parse_let_list_literal(&mut self) -> Result<String, ParseError> {
5077        self.consume(TokenType::LBracket)?;
5078        let mut items = Vec::new();
5079        if !self.check(TokenType::RBracket) {
5080            items.push(self.parse_let_value_expr()?);
5081            while self.check(TokenType::Comma) {
5082                self.advance();
5083                if self.check(TokenType::RBracket) {
5084                    break; // trailing comma
5085                }
5086                items.push(self.parse_let_value_expr()?);
5087            }
5088        }
5089        self.consume(TokenType::RBracket)?;
5090        Ok(format!("[{}]", items.join(", ")))
5091    }
5092
5093    // ── RETURN ───────────────────────────────────────────────────
5094
5095    fn parse_return(&mut self) -> Result<ReturnStatement, ParseError> {
5096        let tok = self.consume(TokenType::Return)?;
5097        let loc = self.loc_of(&tok);
5098        let value = self.parse_let_value_expr()?;
5099        Ok(ReturnStatement {
5100            value_expr: value,
5101            loc,
5102        })
5103    }
5104
5105    // ── TIER 2 FLOW STEP HELPERS ────────────────────────────────────
5106
5107    /// Parse: keyword target (consumes keyword + one identifier/keyword-as-value).
5108    fn parse_flow_step_simple(&mut self, _kw: &str) -> Result<(Loc, String), ParseError> {
5109        let tok = self.current().clone();
5110        self.advance(); // consume keyword
5111        let target = if self.at_declaration_start()
5112            || self.check(TokenType::RBrace)
5113            || self.check(TokenType::Eof)
5114        {
5115            String::new()
5116        } else {
5117            self.consume_any_ident_or_kw()?.value.clone()
5118        };
5119        // Skip optional braced block
5120        if self.check(TokenType::LBrace) {
5121            self.skip_braced_block()?;
5122        }
5123        Ok((
5124            Loc {
5125                line: tok.line,
5126                column: tok.column,
5127            },
5128            target,
5129        ))
5130    }
5131
5132    /// Parse: keyword { ... } — block-level step, skip body structurally.
5133    /// v2.67.0 — `stream { <steps> }` with a REAL body.
5134    ///
5135    /// The four block primitives (`deliberate`, `consensus`, `stream`,
5136    /// `transact`) all went through [`Self::parse_block_step`], whose entire job
5137    /// is `skip_braced_block()`. Their bodies were discarded at parse time — so
5138    /// their handlers were not no-ops through neglect, they were no-ops
5139    /// *by construction*: there was nothing in the AST to execute. v2.67.0 retracted
5140    /// `transact`; this gives `stream` its body back. `deliberate` / `consensus`
5141    /// remain body-less pending their Tier-4 disposition.
5142    fn parse_stream_block(&mut self) -> Result<StreamBlock, ParseError> {
5143        let tok = self.current().clone();
5144        let loc = self.loc_of(&tok);
5145        self.advance(); // consume `stream`
5146
5147        // v2.83.0 — `<T>`: the CHUNK type, and the reason this is not just a
5148        // cosmetic capture. The skip loop below used to eat it: `stream<QuoteData>`
5149        // advanced straight past `<QuoteData>` looking for `{`, so the one piece of
5150        // type information the author wrote about the stream was discarded before
5151        // anything could check it.
5152        let mut chunk_type = String::new();
5153        if self.check(TokenType::Lt) {
5154            self.advance();
5155            let inner = self.parse_type_expr()?;
5156            chunk_type = if inner.generic_param.is_empty() {
5157                inner.name
5158            } else {
5159                format!("{}<{}>", inner.name, inner.generic_param)
5160            };
5161            self.consume(TokenType::Gt)?;
5162        }
5163
5164        // Tolerate the pre-111 form `stream <effect-ish tokens> { … }`: skip any
5165        // argument tokens ahead of the brace, exactly as `parse_block_step` did,
5166        // so an existing program keeps parsing. Only the BODY changes.
5167        while !self.check(TokenType::LBrace)
5168            && !self.check(TokenType::RBrace)
5169            && !self.check(TokenType::Eof)
5170            && !self.at_declaration_start()
5171        {
5172            self.advance();
5173        }
5174
5175        let mut block = StreamBlock {
5176            chunk_type,
5177            on_chunk: None,
5178            on_complete: None,
5179            on_error: None,
5180            body: Vec::new(),
5181            loc,
5182        };
5183
5184        if self.check(TokenType::LBrace) {
5185            self.advance();
5186            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5187                // v2.83.0 — the two SPECIFIED handler arms. `the design plan`'s D8
5188                // promises `stream<τ> { on_chunk: … on_complete: … }` compiles
5189                // with "cero cambios en `.axon` source files de adopters"; before
5190                // this landed it was a hard parse error at flow level and a
5191                // silent discard in a step body.
5192                let name = self.current().value.clone();
5193                let is_arm = matches!(name.as_str(), "on_chunk" | "on_complete" | "on_error")
5194                    && self
5195                        .tokens
5196                        .get(self.pos + 1)
5197                        .is_some_and(|t| t.ttype == TokenType::Colon);
5198                if is_arm {
5199                    let arm_tok = self.current().clone();
5200                    self.advance(); // the handler name
5201                    self.advance(); // `:`
5202                    let arm = self.parse_stream_handler_arm(&name, &arm_tok)?;
5203                    let slot = match name.as_str() {
5204                        "on_chunk" => &mut block.on_chunk,
5205                        "on_complete" => &mut block.on_complete,
5206                        _ => &mut block.on_error,
5207                    };
5208                    if slot.is_some() {
5209                        return Err(ParseError {
5210                            message: format!(
5211                                "`{name}` is declared twice in this `stream` block. Two handlers \
5212                                 for one edge have no defined composition (whose output is the \
5213                                 stream's?), so the duplicate is refused rather than silently \
5214                                 overwriting the first."
5215                            ),
5216                            line: arm_tok.line,
5217                            column: arm_tok.column,
5218                            ..Default::default()
5219                        });
5220                    }
5221                    *slot = Some(arm);
5222                    continue;
5223                }
5224
5225                // A `<ident>: {` that is NOT one of the two arms is a TYPO in a
5226                // closed catalog, and the v2.83.0 discipline says to ask which
5227                // direction the silence fails in: a mis-spelled `on_chunk` would
5228                // fall through to `parse_flow_step` and be reported against the
5229                // brace, pointing the author at the wrong token entirely. Name
5230                // the key and the catalog instead.
5231                let next_two_are_block = self
5232                    .tokens
5233                    .get(self.pos + 1)
5234                    .is_some_and(|t| t.ttype == TokenType::Colon)
5235                    && self
5236                        .tokens
5237                        .get(self.pos + 2)
5238                        .is_some_and(|t| t.ttype == TokenType::LBrace);
5239                if next_two_are_block {
5240                    let bad = self.current().clone();
5241                    return Err(ParseError {
5242                        message: format!(
5243                            "unknown `stream` handler `{name}` — this block accepts only \
5244                             `on_chunk:` (run once per chunk, with the chunk bound as `chunk`), \
5245                             `on_complete:` (run once, after the source closes, with the \
5246                             accumulation bound as `complete`) and `on_error:` (run when the \
5247                             SOURCE fails, with the failure bound as `error`). An unrecognised \
5248                             handler is refused rather than skipped: a skipped handler removes \
5249                             the processing the author wrote, and silence in that direction is \
5250                             indistinguishable from a stream that had nothing to do."
5251                        ),
5252                        line: bad.line,
5253                        column: bad.column,
5254                        ..Default::default()
5255                    });
5256                }
5257
5258                // v2.67.0's body form, kept: `stream { <flow steps> }`.
5259                block.body.push(self.parse_flow_step()?);
5260            }
5261            self.consume(TokenType::RBrace)?;
5262        }
5263
5264        Ok(block)
5265    }
5266
5267    /// v2.83.0 — one `on_chunk:` / `on_complete:` arm, parsed as a STEP body.
5268    ///
5269    /// The arm carries `output:` (README block 15 writes `output: QuoteSnapshot`
5270    /// in `on_chunk` and `output: VerifiedQuote` in `on_complete`), and `output:`
5271    /// is a step field with no flow-level position. Reusing
5272    /// [`Self::parse_step_body_into`] is therefore not a convenience — it is the
5273    /// only shape that accepts what the README publishes, and it means the arm
5274    /// dispatches through `run_step` like any other step.
5275    fn parse_stream_handler_arm(
5276        &mut self,
5277        name: &str,
5278        at: &Token,
5279    ) -> Result<StepNode, ParseError> {
5280        self.consume(TokenType::LBrace)?;
5281        let mut node = StepNode {
5282            name: name.to_string(),
5283            persona_ref: String::new(),
5284            given: String::new(),
5285            ask: String::new(),
5286            output_type: String::new(),
5287            confidence_floor: None,
5288            navigate_ref: String::new(),
5289            apply_ref: String::new(),
5290            requires_context: None,
5291            now_tz: None,
5292            guards: Vec::new(),
5293            pix_ops: Vec::new(),
5294            stream: None,
5295            performs: Vec::new(),
5296            loc: self.loc_of(at),
5297        };
5298        self.parse_step_body_into(&mut node)?;
5299        self.consume(TokenType::RBrace)?;
5300        Ok(node)
5301    }
5302
5303    fn parse_block_step(&mut self, _kw: &str) -> Result<Loc, ParseError> {
5304        let tok = self.current().clone();
5305        self.advance();
5306        // Skip optional arguments before brace
5307        while !self.check(TokenType::LBrace)
5308            && !self.check(TokenType::RBrace)
5309            && !self.check(TokenType::Eof)
5310            && !self.at_declaration_start()
5311        {
5312            self.advance();
5313        }
5314        if self.check(TokenType::LBrace) {
5315            self.skip_braced_block()?;
5316        }
5317        Ok(Loc {
5318            line: tok.line,
5319            column: tok.column,
5320        })
5321    }
5322
5323    /// v2.41.0 — parse `forge <Name>(seed: "<text>") -> <Type> { mode:,
5324    /// novelty:, depth:, branches:, constraints: }`. Real field capture
5325    /// (replacing the pre-v2.41.0 discard-everything stub). Strict closed-catalog:
5326    /// an unknown field is a hard parse error; all cross-field laws (Boden mode
5327    /// catalog, novelty range, depth/branches ≥ 1, `constraints:` → `anchor`)
5328    /// are v2.41.0 type-checker territory.
5329    fn parse_forge_step(&mut self) -> Result<ForgeBlock, ParseError> {
5330        let tok = self.consume(TokenType::Forge)?;
5331        let name = self.consume(TokenType::Identifier)?.value;
5332        let mut node = ForgeBlock {
5333            name,
5334            novelty: 0.5,
5335            depth: 1,
5336            branches: 1,
5337            loc: Loc { line: tok.line, column: tok.column },
5338            ..Default::default()
5339        };
5340        // `(seed: "...")`
5341        self.consume(TokenType::LParen)?;
5342        let arg = self.consume_any_ident_or_kw()?.value;
5343        self.consume(TokenType::Colon)?;
5344        if arg != "seed" {
5345            return Err(self.error(&format!(
5346                "forge '{}' expects `seed:` as its argument, found `{arg}`",
5347                node.name
5348            )));
5349        }
5350        node.seed = self.consume(TokenType::StringLit)?.value;
5351        self.consume(TokenType::RParen)?;
5352        // `-> <Type>`
5353        self.consume(TokenType::Arrow)?;
5354        node.output_type = self.consume_any_ident_or_kw()?.value;
5355        // `{ fields }`
5356        self.consume(TokenType::LBrace)?;
5357        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5358            let field = self.consume_any_ident_or_kw()?.value;
5359            self.consume(TokenType::Colon)?;
5360            match field.as_str() {
5361                "mode" => node.mode = self.consume_any_ident_or_kw()?.value,
5362                "novelty" => node.novelty = self.consume_number()?,
5363                "depth" => {
5364                    node.depth = self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0)
5365                }
5366                "branches" => {
5367                    node.branches =
5368                        self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0)
5369                }
5370                "constraints" => node.constraints_ref = self.consume_any_ident_or_kw()?.value,
5371                other => {
5372                    return Err(self.error(&format!("unknown forge field `{other}`")))
5373                }
5374            }
5375            if self.check(TokenType::Comma) {
5376                self.consume(TokenType::Comma)?;
5377            }
5378        }
5379        self.consume(TokenType::RBrace)?;
5380        Ok(node)
5381    }
5382
5383    /// v2.15.0 — Parse `par { stmt1 stmt2 … }` into CONCURRENT branches.
5384    /// Each top-level flow statement inside the block is one branch (a
5385    /// single-statement body); they execute concurrently at runtime
5386    /// (`flow_dispatcher::parallel::run_branches_concurrently`). Before v2.15.0 the
5387    /// `par` body was skipped (`parse_block_step`), so the branches were lost
5388    /// and the handler ran as a stub. Multi-statement branches (grouping
5389    /// several steps into one sequential branch) are a future grammar
5390    /// extension; today the natural `par { step A  step B }` fans A and B out.
5391    fn parse_par_block(&mut self) -> Result<ParBlock, ParseError> {
5392        let tok = self.current().clone();
5393        self.advance(); // consume `par`
5394        self.consume(TokenType::LBrace)?;
5395        let mut branches: Vec<Vec<FlowStep>> = Vec::new();
5396        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5397            branches.push(vec![self.parse_flow_step()?]);
5398        }
5399        self.consume(TokenType::RBrace)?;
5400        Ok(ParBlock {
5401            branches,
5402            loc: Loc {
5403                line: tok.line,
5404                column: tok.column,
5405            },
5406        })
5407    }
5408
5409    /// v2.4.0 — Parse the `quant` cognitive block surface.
5410    ///
5411    /// Grammar (the attribute header is OPTIONAL):
5412    /// ```text
5413    /// quant { <flow steps> }
5414    /// quant(encoding: amplitude, observable: M, qubits: 10,
5415    ///       depth: 4, bandwidth: 0.5, reupload: 3, backend: quant_sim) { <flow steps> }
5416    /// ```
5417    /// The bare form (the paper's example) leaves every attribute defaulted
5418    /// (`encoding = amplitude`, `effect = quant_sim`). The body is parsed into
5419    /// real nested `FlowStep`s — like `par` branches — so v2.4.0's Continuous
5420    /// Type Invariant scans actual AST rather than skipped tokens.
5421    /// v2.43.0 — parse `warden(<target>) within <Scope> { <body> }`. The
5422    /// `within <Scope>` clause is MANDATORY at the grammar level (fail-closed by
5423    /// construction: a scopeless warden cannot be written); v2.43.0 checks the
5424    /// scope RESOLVES + the target is in its allowlist.
5425    fn parse_warden(&mut self) -> Result<WardenBlock, ParseError> {
5426        let tok = self.consume(TokenType::Warden)?;
5427        // `(<target>)` — the resource under analysis.
5428        self.consume(TokenType::LParen)?;
5429        let target = self.consume_any_ident_or_kw()?.value;
5430        self.consume(TokenType::RParen)?;
5431        // `within <Scope>` — MANDATORY. Omitting it is a hard parse error.
5432        self.consume(TokenType::Within)?;
5433        let scope_ref = self.consume(TokenType::Identifier)?.value;
5434        let mut block = WardenBlock {
5435            target,
5436            scope_ref,
5437            body: Vec::new(),
5438            loc: Loc {
5439                line: tok.line,
5440                column: tok.column,
5441            },
5442        };
5443        // Body: real nested flow steps (like `quant`/`par`).
5444        self.consume(TokenType::LBrace)?;
5445        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5446            block.body.push(self.parse_flow_step()?);
5447        }
5448        self.consume(TokenType::RBrace)?;
5449        Ok(block)
5450    }
5451
5452    /// v2.43.0 — parse `scope <Name> { targets: [ … ], depth: <ident>,
5453    /// approver: [requires] "<cap>" }`. Flat key:value block (the `cache` shape).
5454    /// Catalog + non-empty validation is v2.43.0. Unknown fields are a hard error
5455    ///: a scope governs an offensive-capable analysis.
5456    fn parse_scope(&mut self) -> Result<ScopeDefinition, ParseError> {
5457        let tok = self.consume(TokenType::Scope)?;
5458        let name = self.consume(TokenType::Identifier)?.value;
5459        let mut node = ScopeDefinition {
5460            name,
5461            loc: Loc {
5462                line: tok.line,
5463                column: tok.column,
5464            },
5465            ..Default::default()
5466        };
5467        self.consume(TokenType::LBrace)?;
5468        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5469            let key = self.consume_any_ident_or_kw()?.value;
5470            self.consume(TokenType::Colon)?;
5471            match key.as_str() {
5472                "targets" => {
5473                    self.consume(TokenType::LBracket)?;
5474                    while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
5475                        let t = if self.check(TokenType::StringLit) {
5476                            self.consume(TokenType::StringLit)?.value
5477                        } else {
5478                            self.consume_any_ident_or_kw()?.value
5479                        };
5480                        node.targets.push(t);
5481                        if self.check(TokenType::Comma) {
5482                            self.advance();
5483                        }
5484                    }
5485                    self.consume(TokenType::RBracket)?;
5486                }
5487                "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
5488                "approver" => {
5489                    // Optional `requires` sugar before the capability string.
5490                    if self.current().value == "requires" {
5491                        self.advance();
5492                    }
5493                    node.approver = self.consume(TokenType::StringLit)?.value;
5494                }
5495                other => {
5496                    return Err(self.error(&format!(
5497                        "unknown scope field `{other}` in scope `{}` — expected \
5498                         `targets` / `depth` / `approver`",
5499                        node.name
5500                    )))
5501                }
5502            }
5503            if self.check(TokenType::Comma) {
5504                self.consume(TokenType::Comma)?;
5505            }
5506        }
5507        self.consume(TokenType::RBrace)?;
5508        Ok(node)
5509    }
5510
5511    fn parse_quant(&mut self) -> Result<QuantBlock, ParseError> {
5512        let tok = self.current().clone();
5513        self.advance(); // consume `quant`
5514
5515        let mut block = QuantBlock {
5516            encoding: None,
5517            observable: None,
5518            qubits: None,
5519            depth: None,
5520            bandwidth: None,
5521            reupload: None,
5522            // D1/D9 default backend: the CPU simulator effect. `qpu_native` is
5523            // opt-in via `backend: qpu_native`.
5524            effect: "quant_sim".to_string(),
5525            body: Vec::new(),
5526            loc: Loc {
5527                line: tok.line,
5528                column: tok.column,
5529            },
5530        };
5531
5532        // ── Optional attribute header: `(key: value, …)` ──
5533        if self.check(TokenType::LParen) {
5534            self.advance();
5535            while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
5536                let key = self.consume_any_ident_or_kw()?.value;
5537                self.consume(TokenType::Colon)?;
5538                match key.as_str() {
5539                    "encoding" => {
5540                        block.encoding = Some(self.consume_any_ident_or_kw()?.value)
5541                    }
5542                    "observable" => {
5543                        block.observable = Some(self.parse_dotted_identifier()?)
5544                    }
5545                    "qubits" => block.qubits = Some(self.consume_number()? as i64),
5546                    "depth" => block.depth = Some(self.consume_number()? as i64),
5547                    "bandwidth" => block.bandwidth = Some(self.consume_number()?),
5548                    // v2.23.0 — data re-uploading layers.
5549                    "reupload" => block.reupload = Some(self.consume_number()? as i64),
5550                    // `backend:` selects the algebraic-effect tag (D1/D9).
5551                    "backend" => block.effect = self.consume_any_ident_or_kw()?.value,
5552                    other => {
5553                        return Err(ParseError {
5554                            message: format!(
5555                                "Unknown `quant` attribute `{other}` — expected one of \
5556                                 encoding, observable, qubits, depth, bandwidth, reupload, backend"
5557                            ),
5558                            line: self.current().line,
5559                            column: self.current().column,
5560                            ..Default::default()
5561                        });
5562                    }
5563                }
5564                // Optional comma between attributes (order-free, trailing-comma ok).
5565                if self.check(TokenType::Comma) {
5566                    self.advance();
5567                }
5568            }
5569            self.consume(TokenType::RParen)?;
5570        }
5571
5572        // ── Body: real nested flow steps (like `par`) ──
5573        self.consume(TokenType::LBrace)?;
5574        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5575            block.body.push(self.parse_flow_step()?);
5576        }
5577        self.consume(TokenType::RBrace)?;
5578
5579        Ok(block)
5580    }
5581
5582    /// v2.4.0 — Parse the `yield <expr>` measurement point. Reuses the
5583    /// `let`-value expression grammar (reference / literal / arithmetic) so the
5584    /// yielded value's tokenization intent is preserved in `value_kind`.
5585    fn parse_yield(&mut self) -> Result<YieldStatement, ParseError> {
5586        let tok = self.consume(TokenType::Yield)?;
5587        let loc = self.loc_of(&tok);
5588        self.last_let_value_kind = "literal".to_string();
5589        let value_expr = self.parse_let_value_expr()?;
5590        Ok(YieldStatement {
5591            value_expr,
5592            value_kind: self.last_let_value_kind.clone(),
5593            loc,
5594        })
5595    }
5596
5597    /// Parse: keyword Name on target -> output_type (apply pattern).
5598    /// v2.67.0 — `compute <Name> on <a>, <b>, … -> <out>`.
5599    ///
5600    /// Positional arguments, bound to the compute's declared parameters in order.
5601    /// The generic [`Self::parse_apply_step`] captured a single `on <target>` and
5602    /// then the call site threw even that away (`arguments: Vec::new()`).
5603    fn parse_compute_apply(&mut self) -> Result<ComputeApplyStep, ParseError> {
5604        let tok = self.current().clone();
5605        let loc = self.loc_of(&tok);
5606        self.advance(); // consume `compute`
5607        let compute_name = self.consume_any_ident_or_kw()?.value.clone();
5608
5609        let mut arguments = Vec::new();
5610        if self.current().value == "on" {
5611            self.advance();
5612            loop {
5613                // v2.83.0 — SUBJECT position. README writes
5614                // `compute EligibilityScore on Profile.tenure, Profile.spend,
5615                // Profile.incidents -> score`; the bare-identifier read stopped
5616                // at the first dot, which is why every published `compute`
5617                // application failed on its own argument list.
5618                arguments.push(self.parse_subject()?);
5619                if self.check(TokenType::Comma) {
5620                    self.advance();
5621                } else {
5622                    break;
5623                }
5624            }
5625        }
5626
5627        let mut output_name = String::new();
5628        if self.check(TokenType::Arrow) {
5629            self.advance();
5630            output_name = self.consume_any_ident_or_kw()?.value.clone();
5631        }
5632
5633        Ok(ComputeApplyStep {
5634            compute_name,
5635            arguments,
5636            output_name,
5637            loc,
5638        })
5639    }
5640
5641    /// v4.5.0 — `declassify <Class> from <source> -> <Type> via <Shield>`.
5642    ///
5643    /// Shaped after `shield <S> on <v> -> <out>` on purpose: an adopter who
5644    /// learned one reads the other. What differs is that every part is
5645    /// REQUIRED. An apply-step tolerates a missing target because it still
5646    /// means something; a declassification with no class, no source, no
5647    /// destination type or no authorising shield does not — it would be an
5648    /// assertion about nothing that the coverage laws would then honour.
5649    fn parse_declassify_step(&mut self) -> Result<DeclassifyStep, ParseError> {
5650        let tok = self.current().clone();
5651        self.advance();
5652
5653        let class = self.consume_any_ident_or_kw()?.value.clone();
5654
5655        if self.current().value != "from" {
5656            return Err(ParseError {
5657                message: format!(
5658                    "`declassify {class}` must name the value it retires the class from: \
5659                     `declassify {class} from <value> -> <Type> via <Shield>`. Found `{}`.",
5660                    self.current().value
5661                ),
5662                line: self.current().line,
5663                column: self.current().column,
5664                source_snippet: None,
5665            });
5666        }
5667        self.advance();
5668        let source = self.parse_subject()?;
5669
5670        if !self.check(TokenType::Arrow) {
5671            return Err(ParseError {
5672                message: format!(
5673                    "`declassify {class} from {source}` must name the TYPE the value leaves \
5674                     as: `-> <Type>`. A declassification that does not change the type \
5675                     changes nothing a trust boundary can read."
5676                ),
5677                line: self.current().line,
5678                column: self.current().column,
5679                source_snippet: None,
5680            });
5681        }
5682        self.advance();
5683        let output_type = self.consume_any_ident_or_kw()?.value.clone();
5684
5685        if self.current().value != "via" {
5686            return Err(ParseError {
5687                message: format!(
5688                    "`declassify {class} from {source} -> {output_type}` must name the \
5689                     shield that authorises it: `via <Shield>`. Retiring a regulatory \
5690                     class is a capability a control declares, not something a step may \
5691                     do on its own."
5692                ),
5693                line: self.current().line,
5694                column: self.current().column,
5695                source_snippet: None,
5696            });
5697        }
5698        self.advance();
5699        let shield = self.consume_any_ident_or_kw()?.value.clone();
5700
5701        Ok(DeclassifyStep {
5702            class,
5703            source,
5704            output_type,
5705            shield,
5706            loc: Loc { line: tok.line, column: tok.column },
5707        })
5708    }
5709    fn parse_apply_step(&mut self, _kw: &str) -> Result<(Loc, String, String, String), ParseError> {
5710        let tok = self.current().clone();
5711        self.advance(); // consume keyword
5712        let name = self.consume_any_ident_or_kw()?.value.clone();
5713        let mut target = String::new();
5714        let mut output_type = String::new();
5715        // "on" target
5716        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
5717            let next = self.current().clone();
5718            if next.value == "on" {
5719                self.advance();
5720                // v2.83.0 — SUBJECT position (the name before `on` is a
5721                // NAME and stays bare).
5722                target = self.parse_subject()?;
5723            }
5724        }
5725        // -> output_type
5726        if self.check(TokenType::Arrow) {
5727            self.advance();
5728            output_type = self.consume_any_ident_or_kw()?.value.clone();
5729        }
5730        // Skip optional braced block
5731        if self.check(TokenType::LBrace) {
5732            self.skip_braced_block()?;
5733        }
5734        Ok((
5735            Loc {
5736                line: tok.line,
5737                column: tok.column,
5738            },
5739            name,
5740            target,
5741            output_type,
5742        ))
5743    }
5744
5745    /// v2.83.0 — `<kind> <Name> [on <target>] [-> <binding>]` inside
5746    /// a `step { }` body.
5747    ///
5748    /// Differences from the flow-level `parse_apply_step`, both deliberate:
5749    ///
5750    /// - The target may be a CALL EXPRESSION, captured verbatim: README block
5751    ///   42 writes `mandate LegalPrecision on ContractDrafter(terms)`. The
5752    ///   flow-level form never needed this; the published step-level form does.
5753    /// - No trailing braced block is skipped. A guard is one statement; a
5754    /// silently-skipped block after it would be the v2.83.0 defect again.
5755    fn parse_step_guard(&mut self, kind: &str) -> Result<StepGuardNode, ParseError> {
5756        let tok = self.current().clone();
5757        self.advance(); // consume the keyword
5758        let name = self.consume_any_ident_or_kw()?.value.clone();
5759        let mut target = String::new();
5760        let mut binding = String::new();
5761        if self.current().value == "on" {
5762            self.advance();
5763            // v2.83.0 — SUBJECT position. `shield S on vital_event -> safe`
5764            // already worked; `shield S on Charge.output -> x` did not.
5765            target = self.parse_subject()?;
5766            // `ContractDrafter(terms)` — capture the balanced argument list
5767            // verbatim into the target string.
5768            if self.check(TokenType::LParen) {
5769                let mut depth = 0usize;
5770                loop {
5771                    let t = self.current().clone();
5772                    match t.ttype {
5773                        TokenType::LParen => depth += 1,
5774                        TokenType::RParen => depth -= 1,
5775                        TokenType::Eof => {
5776                            return Err(ParseError {
5777                                message: format!(
5778                                    "unterminated argument list in `{kind} {name} on {target}(…`"
5779                                ),
5780                                line: t.line,
5781                                column: t.column,
5782                                ..Default::default()
5783                            })
5784                        }
5785                        _ => {}
5786                    }
5787                    target.push_str(&t.value);
5788                    self.advance();
5789                    if depth == 0 {
5790                        break;
5791                    }
5792                }
5793            }
5794        }
5795        if self.check(TokenType::Arrow) {
5796            self.advance();
5797            binding = self.consume_any_ident_or_kw()?.value.clone();
5798        }
5799        Ok(StepGuardNode {
5800            kind: kind.to_string(),
5801            name,
5802            target,
5803            binding,
5804            loc: Loc {
5805                line: tok.line,
5806                column: tok.column,
5807            },
5808        })
5809    }
5810
5811    /// v2.83.0 — `reason [<target>] [{ given: … ask: "…" depth: N }]`.
5812    ///
5813    /// Replaces the `parse_flow_step_simple("reason")` call whose entire
5814    /// treatment of the block was `skip_braced_block()`. Sixteen README blocks
5815    /// write the braced form and every one of them lowered to an empty prompt.
5816    ///
5817    /// The field set is CLOSED. An unrecognised key is an ERROR that names the
5818    /// key and lists what is accepted — the v2.83.0 discipline: a skipped
5819    /// field in a deliberation removes the deliberation (a promptless `reason`
5820    /// is silent, not loud), so the silent direction is the dangerous one.
5821    fn parse_reason_step(&mut self) -> Result<ReasonStep, ParseError> {
5822        let tok = self.current().clone();
5823        let loc = self.loc_of(&tok);
5824        self.advance(); // consume `reason`
5825
5826        // The pre-v2.83.0 positional form: `reason <target>`. Absent when the
5827        // block follows immediately, which is how the README always writes it.
5828        //
5829        // The `Colon` lookahead matters: a bare `reason` on its own line inside
5830        // a `step { }` body is followed by the step's NEXT FIELD, and without
5831        // this guard the target would swallow that field's key (`output`) and
5832        // the step would then fail on a stray `:` — an error pointing two
5833        // tokens past the actual problem. `skip_flow_step_structural` used to
5834        // absorb this shape silently; a wrong diagnostic is not an improvement
5835        // on a silent drop.
5836        let next_is_field_key = self
5837            .tokens
5838            .get(self.pos + 1)
5839            .is_some_and(|t| t.ttype == TokenType::Colon);
5840        let target = if self.check(TokenType::LBrace)
5841            || self.at_declaration_start()
5842            || self.check(TokenType::RBrace)
5843            || self.check(TokenType::Eof)
5844            || next_is_field_key
5845        {
5846            String::new()
5847        } else {
5848            self.parse_dotted_identifier()?
5849        };
5850
5851        let mut node = ReasonStep {
5852            strategy: String::new(),
5853            target,
5854            given: String::new(),
5855            ask: String::new(),
5856            depth: None,
5857            loc,
5858        };
5859
5860        if !self.check(TokenType::LBrace) {
5861            return Ok(node);
5862        }
5863        self.advance();
5864        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5865            let key = self.current().clone();
5866            self.advance();
5867            self.consume(TokenType::Colon)?;
5868            match key.value.as_str() {
5869                // `given: A.output`, `given: A.output, sessions`,
5870                // `given: [baseline.topology, current.topology]` — all three
5871                // published shapes, normalised to one comma-joined string (the
5872                // same carrier `StepNode.given` already uses).
5873                "given" => {
5874                    let mut parts = vec![self.parse_expression_string()?];
5875                    while self.check(TokenType::Comma) {
5876                        self.advance();
5877                        parts.push(self.parse_expression_string()?);
5878                    }
5879                    node.given = parts.join(", ");
5880                }
5881                "ask" => node.ask = self.consume(TokenType::StringLit)?.value,
5882                "depth" => {
5883                    let n = self.current().clone();
5884                    if n.ttype != TokenType::Integer {
5885                        return Err(ParseError {
5886                            message: format!(
5887                                "`depth:` in a `reason` block is a deliberation depth — a \
5888                                 positive integer (got '{}')",
5889                                n.value
5890                            ),
5891                            line: n.line,
5892                            column: n.column,
5893                            ..Default::default()
5894                        });
5895                    }
5896                    self.advance();
5897                    node.depth = n.value.parse::<u32>().ok();
5898                }
5899                // `chain_of_thought: enabled` is the README's spelling of a
5900                // named strategy; `strategy: <name>` is the general form. Both
5901                // land in the same field because dispatch reads one posture.
5902                "chain_of_thought" => {
5903                    let v = self.consume_any_ident_or_kw()?.value;
5904                    if v == "enabled" {
5905                        node.strategy = "chain_of_thought".to_string();
5906                    }
5907                }
5908                "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value,
5909                // `target:` is the SUBJECT — the same field the positional
5910                // `reason <target>` form fills, spelled as a key. The parity
5911                // corpus writes it (`reason about_policy { target: "…" }`) and
5912                // the block was discarded whole, so the key has never meant
5913                // anything. Giving it BOTH ways is refused rather than resolved
5914                // by fiat: two spellings of one field with different values
5915                // have no defined winner, and picking one silently is how a
5916                // program comes to mean something its author did not write.
5917                "target" => {
5918                    let v = if self.check(TokenType::StringLit) {
5919                        self.consume(TokenType::StringLit)?.value
5920                    } else {
5921                        self.parse_dotted_identifier()?
5922                    };
5923                    if !node.target.is_empty() {
5924                        return Err(ParseError {
5925                            message: format!(
5926                                "`reason {} {{ target: … }}` declares the subject twice — \
5927                                 once positionally as `{}` and once as `target: {}`. They \
5928                                 are the same field. Write one of them.",
5929                                node.target, node.target, v
5930                            ),
5931                            line: key.line,
5932                            column: key.column,
5933                            ..Default::default()
5934                        });
5935                    }
5936                    node.target = v;
5937                }
5938                other => {
5939                    return Err(ParseError {
5940                        message: format!(
5941                            "unknown field '{other}' in a `reason` block. Accepted: given, \
5942                             ask, depth, strategy, chain_of_thought, target. A field this \
5943                             block does not recognise is REFUSED rather than skipped — a \
5944                             `reason` that silently loses its `ask:` deliberates over \
5945                             nothing, and that failure is quiet."
5946                        ),
5947                        line: key.line,
5948                        column: key.column,
5949                        ..Default::default()
5950                    })
5951                }
5952            }
5953        }
5954        self.consume(TokenType::RBrace)?;
5955        Ok(node)
5956    }
5957
5958    /// v2.83.0 — the CLOSED braceless catalog for `weave`.
5959    ///
5960    /// `output` is deliberately ABSENT, for the reason `at_navigate_field`
5961    /// already records: in step-body position `output:` is the STEP's own
5962    /// field, and a shared name makes the terminator ambiguous. This is not
5963    /// hypothetical here — it is the exact bug the old skipper had, from the
5964    /// other side: `skip_flow_step_structural` STOPPED at `output`, mid-list,
5965    /// and the step then failed on a stray comma.
5966    fn at_weave_field(&self) -> bool {
5967        const FIELDS: &[&str] = &["format", "include", "priority", "style"];
5968        self.field_ahead(FIELDS)
5969    }
5970
5971    /// v2.83.0 — `weave [a, b] [into <T>] [format: … include: […]]`.
5972    ///
5973    /// Three published surfaces, one implementation:
5974    ///   - the step-body statement — `weave [A.output, B.output]` followed by a
5975    ///     braceless `format:` / `include:` list (14 README blocks);
5976    ///   - the flow-body statement — `weave [A, B] into Report { format: T }`;
5977    ///   - the braced field form `weave { sources: […] … }`, which no published
5978    /// block writes but which predates this cycle and keeps working.
5979    fn parse_weave_step(&mut self) -> Result<FlowStep, ParseError> {
5980        let tok = self.current().clone();
5981        self.advance();
5982        let mut node = WeaveStep {
5983            sources: Vec::new(),
5984            target: String::new(),
5985            format_type: String::new(),
5986            priority: Vec::new(),
5987            style: String::new(),
5988            include: Vec::new(),
5989            loc: Loc {
5990                line: tok.line,
5991                column: tok.column,
5992            },
5993        };
5994        // `weave [A.output, B.output]` — the sources are REFERENCES, so they
5995        // are dotted. `parse_bracketed_dot_identifiers` is the same helper
5996        // `given:` uses; the pre-v2.83.0 braced form's `sources:` used the
5997        // non-dotted one, which is why a dotted source never had a spelling
5998        // that reached the AST.
5999        if self.check(TokenType::LBracket) {
6000            node.sources = self.parse_bracketed_dot_identifiers()?;
6001        } else if self.current().ttype == TokenType::Identifier
6002            && !self
6003                .tokens
6004                .get(self.pos + 1)
6005                .is_some_and(|t| t.ttype == TokenType::Colon)
6006        {
6007            // `weave Baz` — the bare positional subject every other statement
6008            // in the language takes (`probe X`, `reason X`, `validate X`), read
6009            // here as a one-element source list. It is the uniform rule, not a
6010            // special case, and it keeps parsing the shape that used to vanish
6011            // into `skip_flow_step_structural`.
6012            //
6013            // The Colon lookahead is the same guard `parse_reason_step` needs:
6014            // without it a bare `weave` would swallow the enclosing step's next
6015            // field KEY as its source.
6016            node.sources = vec![self.parse_dotted_identifier()?];
6017        }
6018        // `into <Target>` — the flow-level form's destination binding.
6019        if self.check(TokenType::Into) || self.current().value == "into" {
6020            self.advance();
6021            node.target = self.parse_dotted_identifier()?;
6022        }
6023        // The braceless continuation, terminated by the closed field catalog.
6024        while self.at_weave_field() {
6025            let f = self.current().value.clone();
6026            self.advance();
6027            self.consume(TokenType::Colon)?;
6028            match f.as_str() {
6029                "format" => node.format_type = self.consume_any_ident_or_kw()?.value.clone(),
6030                "include" => node.include = self.parse_bracketed_dot_identifiers()?,
6031                "priority" => node.priority = self.parse_bracketed_dot_identifiers()?,
6032                "style" => node.style = self.consume_any_ident_or_kw()?.value.clone(),
6033                // `at_weave_field` is the gate above; this arm is unreachable
6034                // unless the two catalogs drift apart.
6035                other => {
6036                    return Err(ParseError {
6037                        message: format!(
6038                            "`{other}` passed the `weave` field test but has no handler — \
6039                             the braceless catalog and its parser have drifted apart."
6040                        ),
6041                        line: tok.line,
6042                        column: tok.column,
6043                        ..Default::default()
6044                    })
6045                }
6046            }
6047        }
6048        if self.check(TokenType::LBrace) {
6049            self.advance();
6050            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6051                let f = self.current().value.clone();
6052                self.advance();
6053                if self.check(TokenType::Colon) {
6054                    self.advance();
6055                    match f.as_str() {
6056                        "sources" => node.sources = self.parse_bracketed_dot_identifiers()?,
6057                        "target" => node.target = self.consume_any_ident_or_kw()?.value.clone(),
6058                        "format" => {
6059                            node.format_type = self.consume_any_ident_or_kw()?.value.clone()
6060                        }
6061                        "priority" => node.priority = self.parse_bracketed_dot_identifiers()?,
6062                        "style" => node.style = self.consume_any_ident_or_kw()?.value.clone(),
6063                        // v2.83.0 — the braced form takes `include:` too,
6064                        // so the two spellings of one construct cannot disagree
6065                        // about which fields exist.
6066                        "include" => node.include = self.parse_bracketed_dot_identifiers()?,
6067                        _ => self.skip_value(),
6068                    }
6069                }
6070            }
6071            if self.check(TokenType::RBrace) {
6072                self.advance();
6073            }
6074        }
6075        Ok(FlowStep::Weave(node))
6076    }
6077
6078    fn parse_use_step(&mut self) -> Result<FlowStep, ParseError> {
6079        let tok = self.current().clone();
6080        self.advance();
6081        let tool_name = self.consume_any_ident_or_kw()?.value.clone();
6082        // v2.8.0 — two mutually-exclusive `use` argument surfaces:
6083        //   * `use Tool(query = "${q}", max_results = 5)` — D2 canonical
6084        // multi-field keyword args (v2.8.0 `UseArgs::Named`).
6085        // * `use Tool on "${arg}"` / `on query` — the v2.7.0 single positional
6086        //     argument (D5 back-compat, `UseArgs::LegacyPositional`):
6087        //       - a STRING LITERAL carrying interpolation (`on "${query}"`)
6088        //         resolved at dispatch against request-bound flow params;
6089        //       - a BARE identifier / literal (`on query` / `on 42`) verbatim.
6090        //     (Unquoted `${query}` is intentionally NOT a form — interpolation
6091        //     lives inside string literals everywhere in Axon.)
6092        let args = if self.check(TokenType::LParen) {
6093            UseArgs::Named(self.parse_named_arg_list()?)
6094        } else {
6095            let mut argument = String::new();
6096            if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6097                let next = self.current().clone();
6098                if next.value == "on" {
6099                    self.advance();
6100                    argument = self.consume_any_ident_or_kw()?.value.clone();
6101                }
6102            }
6103            UseArgs::LegacyPositional(argument)
6104        };
6105        if self.check(TokenType::LBrace) {
6106            self.skip_braced_block()?;
6107        }
6108        Ok(FlowStep::UseTool(UseToolStep {
6109            tool_name,
6110            args,
6111            loc: Loc {
6112                line: tok.line,
6113                column: tok.column,
6114            },
6115        }))
6116    }
6117
6118    /// v2.8.0 — parse `(name = value, …)` keyword args for the canonical
6119    /// `use Tool(...)` multi-field dispatch. Values are captured as expression
6120    /// strings (StringLit / Integer / Float / Bool / dotted identifier / list)
6121    /// via the shared `parse_let_atom`, since the frontend has no structured
6122    /// `Expr`. A trailing comma is tolerated; `()` yields no args.
6123    fn parse_named_arg_list(&mut self) -> Result<Vec<(String, String, String)>, ParseError> {
6124        self.consume(TokenType::LParen)?;
6125        let mut args = Vec::new();
6126        while !self.check(TokenType::RParen) {
6127            // Accept a keyword-as-name (`filter`, `type`, `from`, …) — real
6128            // adopter schemas use such names; the following `=` disambiguates.
6129            let name = self.consume_any_ident_or_kw()?.value;
6130            self.consume(TokenType::Assign)?;
6131            let value = self.parse_let_atom()?;
6132            // v2.10.0 — `parse_let_atom` classified the value (`"literal"` vs
6133            // `"reference"`); carry it so the runtime resolves a bare
6134            // identifier / `Step.output` as a binding lookup, not a literal.
6135            let value_kind = self.last_let_value_kind.clone();
6136            args.push((name, value, value_kind));
6137            if self.check(TokenType::Comma) {
6138                self.advance();
6139            } else {
6140                break;
6141            }
6142        }
6143        self.consume(TokenType::RParen)?;
6144        Ok(args)
6145    }
6146
6147    fn parse_remember_step(&mut self) -> Result<FlowStep, ParseError> {
6148        let tok = self.current().clone();
6149        self.advance();
6150        let expr = self.consume_any_ident_or_kw()?.value.clone();
6151        let mut mem = String::new();
6152        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6153            let next = self.current().clone();
6154            if next.value == "in" || next.ttype == TokenType::In {
6155                self.advance();
6156                mem = self.consume_any_ident_or_kw()?.value.clone();
6157            }
6158        }
6159        Ok(FlowStep::Remember(RememberStep {
6160            expression: expr,
6161            memory_target: mem,
6162            loc: Loc {
6163                line: tok.line,
6164                column: tok.column,
6165            },
6166        }))
6167    }
6168
6169    fn parse_recall_step(&mut self) -> Result<FlowStep, ParseError> {
6170        let tok = self.current().clone();
6171        self.advance();
6172        let query = if self.check(TokenType::StringLit) {
6173            self.consume(TokenType::StringLit)?.value.clone()
6174        } else {
6175            self.consume_any_ident_or_kw()?.value.clone()
6176        };
6177        let mut mem = String::new();
6178        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6179            let next = self.current().clone();
6180            if next.value == "from" || next.ttype == TokenType::From {
6181                self.advance();
6182                mem = self.consume_any_ident_or_kw()?.value.clone();
6183            }
6184        }
6185        Ok(FlowStep::Recall(RecallStep {
6186            query,
6187            memory_source: mem,
6188            loc: Loc {
6189                line: tok.line,
6190                column: tok.column,
6191            },
6192        }))
6193    }
6194
6195    fn parse_hibernate_step(&mut self) -> Result<FlowStep, ParseError> {
6196        let tok = self.current().clone();
6197        self.advance();
6198        let mut event = String::new();
6199        let mut timeout = String::new();
6200        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6201            // v2.83.0 — README III writes `hibernate until "event_name"`
6202            // (the `until` keyword + a STRING event). The parser accepted only
6203            // the bare-identifier form, so the published block never compiled.
6204            // Both forms resolve to the same field.
6205            let first = self.consume_any_ident_or_kw()?.value.clone();
6206            if first == "until" && self.check(TokenType::StringLit) {
6207                event = self.consume(TokenType::StringLit)?.value.clone();
6208            } else {
6209                event = first;
6210            }
6211        }
6212        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6213            let next = self.current().clone();
6214            if next.ttype == TokenType::Duration {
6215                self.advance();
6216                timeout = next.value.clone();
6217            }
6218        }
6219        Ok(FlowStep::Hibernate(HibernateStep {
6220            event_name: event,
6221            timeout,
6222            loc: Loc {
6223                line: tok.line,
6224                column: tok.column,
6225            },
6226        }))
6227    }
6228
6229    /// v2.63.0 — `focus <Dataspace> { where: "<filter>", select: [cols], as: <name> }`
6230    /// — σ_φ ∘ π_v over a declared dataspace. The `where:` string is the
6231    /// v1.30.0 data-plane filter grammar (the design decision, shared with retrieve /
6232    /// navigate). Pre-108.d the optional body was silently discarded.
6233    /// v2.65.0 — `grad <letName> wrt <x> [as <name>]` /
6234    /// `grad <letName> wrt [a, b] as <name>`. The differentiation itself
6235    /// happens at CHECK/IR time (T931/T932 + the symbolic differentiator);
6236    /// the parser only captures the surface.
6237    fn parse_grad_step(&mut self) -> Result<FlowStep, ParseError> {
6238        let tok = self.current().clone();
6239        self.advance();
6240        let target = self.consume_any_ident_or_kw()?.value.clone();
6241        let mut wrt: Vec<String> = Vec::new();
6242        let mut output = String::new();
6243        if !self.at_declaration_start() && self.current().value == "wrt" {
6244            self.advance();
6245            if self.check(TokenType::LBracket) {
6246                wrt = self.parse_bracketed_identifiers()?;
6247            } else {
6248                wrt.push(self.consume_any_ident_or_kw()?.value.clone());
6249            }
6250        }
6251        if !self.at_declaration_start() && self.current().value == "as" {
6252            self.advance();
6253            output = self.consume_any_ident_or_kw()?.value.clone();
6254        }
6255        Ok(FlowStep::Grad(GradStep {
6256            target,
6257            wrt,
6258            output,
6259            loc: Loc {
6260                line: tok.line,
6261                column: tok.column,
6262            },
6263        }))
6264    }
6265
6266    fn parse_focus_step(&mut self) -> Result<FlowStep, ParseError> {
6267        let tok = self.current().clone();
6268        self.advance();
6269        let expression = if self.at_declaration_start()
6270            || self.check(TokenType::RBrace)
6271            || self.check(TokenType::Eof)
6272        {
6273            String::new()
6274        } else {
6275            self.consume_any_ident_or_kw()?.value.clone()
6276        };
6277        let mut where_expr = String::new();
6278        let mut select: Vec<String> = Vec::new();
6279        let mut output = String::new();
6280        if self.check(TokenType::LBrace) {
6281            self.advance();
6282            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6283                if self.check(TokenType::Comma) {
6284                    self.advance();
6285                    continue;
6286                }
6287                let f = self.current().value.clone();
6288                self.advance();
6289                if self.check(TokenType::Colon) {
6290                    self.advance();
6291                    match f.as_str() {
6292                        "where" => {
6293                            where_expr = self.consume(TokenType::StringLit)?.value.clone()
6294                        }
6295                        "select" => select = self.parse_bracketed_identifiers()?,
6296                        "as" | "alias" => {
6297                            output = self.consume_any_ident_or_kw()?.value.clone()
6298                        }
6299                        _ => self.skip_value(),
6300                    }
6301                }
6302            }
6303            if self.check(TokenType::RBrace) {
6304                self.advance();
6305            }
6306        }
6307        Ok(FlowStep::Focus(FocusStep {
6308            expression,
6309            where_expr,
6310            select,
6311            output,
6312            loc: Loc {
6313                line: tok.line,
6314                column: tok.column,
6315            },
6316        }))
6317    }
6318
6319    fn parse_associate_step(&mut self) -> Result<FlowStep, ParseError> {
6320        let tok = self.current().clone();
6321        self.advance();
6322        let left = self.consume_any_ident_or_kw()?.value.clone();
6323        let mut right = String::new();
6324        let mut using = String::new();
6325        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6326            right = self.consume_any_ident_or_kw()?.value.clone();
6327        }
6328        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6329            let next = self.current().clone();
6330            if next.value == "using" {
6331                self.advance();
6332                using = self.consume_any_ident_or_kw()?.value.clone();
6333            }
6334        }
6335        let mut output = String::new();
6336        if self.check(TokenType::LBrace) {
6337            self.advance();
6338            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6339                let f = self.current().value.clone();
6340                self.advance();
6341                if self.check(TokenType::Colon) {
6342                    self.advance();
6343                    match f.as_str() {
6344                        "as" | "alias" => output = self.consume_any_ident_or_kw()?.value.clone(),
6345                        _ => self.skip_value(),
6346                    }
6347                }
6348            }
6349            if self.check(TokenType::RBrace) {
6350                self.advance();
6351            }
6352        }
6353        Ok(FlowStep::Associate(AssociateStep {
6354            left,
6355            right,
6356            using_field: using,
6357            output,
6358            loc: Loc {
6359                line: tok.line,
6360                column: tok.column,
6361            },
6362        }))
6363    }
6364
6365    fn parse_aggregate_step(&mut self) -> Result<FlowStep, ParseError> {
6366        let tok = self.current().clone();
6367        self.advance();
6368        let target = self.consume_any_ident_or_kw()?.value.clone();
6369        let mut group_by = Vec::new();
6370        let mut alias = String::new();
6371        let mut compute: Vec<String> = Vec::new();
6372        let mut where_expr = String::new();
6373        if self.check(TokenType::LBrace) {
6374            self.advance();
6375            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6376                let f = self.current().value.clone();
6377                self.advance();
6378                if self.check(TokenType::Colon) {
6379                    self.advance();
6380                    match f.as_str() {
6381                        "group_by" => group_by = self.parse_bracketed_identifiers()?,
6382                        "alias" | "as" => alias = self.consume_any_ident_or_kw()?.value.clone(),
6383                        // v2.63.0 — the closed aggregate catalog, kept
6384                        // RAW (`count`, `sum(score)`, …); T930 validates.
6385                        "compute" => compute = self.parse_bracketed_aggregates()?,
6386                        // v2.63.0 — the data-plane where.
6387                        "where" => where_expr = self.consume(TokenType::StringLit)?.value.clone(),
6388                        _ => self.skip_value(),
6389                    }
6390                }
6391            }
6392            if self.check(TokenType::RBrace) {
6393                self.advance();
6394            }
6395        }
6396        Ok(FlowStep::Aggregate(AggregateStep {
6397            target,
6398            group_by,
6399            alias,
6400            compute,
6401            where_expr,
6402            loc: Loc {
6403                line: tok.line,
6404                column: tok.column,
6405            },
6406        }))
6407    }
6408
6409    fn parse_explore_step(&mut self) -> Result<FlowStep, ParseError> {
6410        let tok = self.current().clone();
6411        self.advance();
6412        let target = self.consume_any_ident_or_kw()?.value.clone();
6413        let mut limit = None;
6414        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6415            if self.current().ttype == TokenType::Integer {
6416                limit = self.current().value.parse::<i64>().ok();
6417                self.advance();
6418            }
6419        }
6420        let mut output = String::new();
6421        if self.check(TokenType::LBrace) {
6422            self.advance();
6423            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6424                let f = self.current().value.clone();
6425                self.advance();
6426                if self.check(TokenType::Colon) {
6427                    self.advance();
6428                    match f.as_str() {
6429                        "as" | "alias" => output = self.consume_any_ident_or_kw()?.value.clone(),
6430                        _ => self.skip_value(),
6431                    }
6432                }
6433            }
6434            if self.check(TokenType::RBrace) {
6435                self.advance();
6436            }
6437        }
6438        Ok(FlowStep::ExploreStep(ExploreStepNode {
6439            target,
6440            limit,
6441            output,
6442            loc: Loc {
6443                line: tok.line,
6444                column: tok.column,
6445            },
6446        }))
6447    }
6448
6449    /// v2.63.0 — parse `[count, sum(score), avg(x)]`: bracketed
6450    /// aggregate entries, each `ident` or `ident(ident)`, kept raw.
6451    fn parse_bracketed_aggregates(&mut self) -> Result<Vec<String>, ParseError> {
6452        let mut out = Vec::new();
6453        self.consume(TokenType::LBracket)?;
6454        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
6455            let name = self.consume_any_ident_or_kw()?.value.clone();
6456            if self.check(TokenType::LParen) {
6457                self.advance();
6458                let col = self.consume_any_ident_or_kw()?.value.clone();
6459                self.consume(TokenType::RParen)?;
6460                out.push(format!("{name}({col})"));
6461            } else {
6462                out.push(name);
6463            }
6464            if self.check(TokenType::Comma) {
6465                self.advance();
6466            }
6467        }
6468        self.consume(TokenType::RBracket)?;
6469        Ok(out)
6470    }
6471
6472    /// v2.63.0 — the governed ingest step:
6473    ///
6474    /// ```text
6475    /// ingest <sourceRef> into <Dataspace> {
6476    ///     format: csv | json
6477    ///     limits { max_bytes: N, max_rows: N }
6478    /// }
6479    /// ```
6480    ///
6481    /// Until 108.c the body was consumed by `skip_braced_block()`. Now it
6482    /// is a closed grammar: `format:` (raw here; required + validated by
6483    /// `axon-T929`) and an optional `limits { … }` block whose bounds are
6484    /// enforced on the raw byte stream BEFORE parsing. An unknown
6485    /// body entry is a parse error.
6486    fn parse_ingest_step(&mut self) -> Result<FlowStep, ParseError> {
6487        let tok = self.current().clone();
6488        self.advance();
6489        let source = self.consume_any_ident_or_kw()?.value.clone();
6490        let mut target = String::new();
6491        let mut format = String::new();
6492        let mut max_bytes: Option<u64> = None;
6493        let mut max_rows: Option<u64> = None;
6494        if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6495            let next = self.current().clone();
6496            if next.value == "into" || next.ttype == TokenType::Into {
6497                self.advance();
6498                target = self.consume_any_ident_or_kw()?.value.clone();
6499            }
6500        }
6501        if self.check(TokenType::LBrace) {
6502            self.consume(TokenType::LBrace)?;
6503            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6504                // Optional separators between body entries.
6505                if self.check(TokenType::Comma) {
6506                    self.advance();
6507                    continue;
6508                }
6509                let entry = self.current().clone();
6510                match entry.value.as_str() {
6511                    "format" => {
6512                        self.advance();
6513                        self.consume(TokenType::Colon)?;
6514                        format = self.consume_any_ident_or_kw()?.value.clone();
6515                    }
6516                    "limits" => {
6517                        self.advance();
6518                        self.consume(TokenType::LBrace)?;
6519                        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6520                            let bound = self.current().clone();
6521                            self.advance();
6522                            self.consume(TokenType::Colon)?;
6523                            let num_tok = self.consume(TokenType::Integer)?.clone();
6524                            let value = num_tok.value.parse::<u64>().map_err(|_| ParseError {
6525                                message: format!(
6526                                    "ingest `limits` bound `{}` must be a non-negative \
6527                                     integer byte/row count, got `{}`.",
6528                                    bound.value, num_tok.value
6529                                ),
6530                                line: num_tok.line,
6531                                column: num_tok.column,
6532                                ..Default::default()
6533                            })?;
6534                            match bound.value.as_str() {
6535                                "max_bytes" => max_bytes = Some(value),
6536                                "max_rows" => max_rows = Some(value),
6537                                other => {
6538                                    return Err(ParseError {
6539                                        message: format!(
6540                                            "Unknown ingest limit `{other}`. The closed \
6541                                             limits grammar is `max_bytes: <N>` and \
6542                                             `max_rows: <N>` — bounds enforced on the raw \
6543                                             stream BEFORE parsing.",
6544                                        ),
6545                                        line: bound.line,
6546                                        column: bound.column,
6547                                        ..Default::default()
6548                                    });
6549                                }
6550                            }
6551                            if self.check(TokenType::Comma) {
6552                                self.advance();
6553                            }
6554                        }
6555                        self.consume(TokenType::RBrace)?;
6556                    }
6557                    other => {
6558                        return Err(ParseError {
6559                            message: format!(
6560                                "Unknown entry `{other}` in ingest body. The closed \
6561                                 grammar is `format: csv|json` and \
6562                                 `limits {{ max_bytes: <N>, max_rows: <N> }}`.",
6563                            ),
6564                            line: entry.line,
6565                            column: entry.column,
6566                            ..Default::default()
6567                        });
6568                    }
6569                }
6570            }
6571            self.consume(TokenType::RBrace)?;
6572        }
6573        Ok(FlowStep::Ingest(IngestStep {
6574            source,
6575            target,
6576            format,
6577            max_bytes,
6578            max_rows,
6579            loc: Loc {
6580                line: tok.line,
6581                column: tok.column,
6582            },
6583        }))
6584    }
6585
6586    /// v2.83.0 — is the cursor on a `navigate` field (`<name>:`)?
6587    ///
6588    /// The continuation test for the braceless field list. Closed catalog by
6589    /// construction: a name outside it ends the navigate and belongs to the
6590    /// enclosing step, which is exactly what makes the delimiter-free form
6591    /// unambiguous.
6592    fn at_navigate_field(&self) -> bool {
6593        const FIELDS: &[&str] = &[
6594            // v2.83.0 — `output` is deliberately ABSENT from the
6595            // BRACELESS catalog even though the braced form accepts it as an
6596            // alias for `as`. In step-body position `output:` is the STEP's
6597            // own field, and a shared name would make the terminator
6598            // ambiguous — the braceless navigate would swallow the step's
6599            // output type. README writes `as:` in this position throughout;
6600            // the braced/flow-level form keeps both spellings.
6601            "corpus", "query", "trail", "as", "from", "budget", "where",
6602            "depth", "recall",
6603        ];
6604        self.field_ahead(FIELDS)
6605    }
6606
6607    /// v2.83.0 — the same test for `drill`.
6608    fn at_drill_field(&self) -> bool {
6609        // Same reason as `at_navigate_field`: no `output` in the braceless
6610        // catalog, because that name belongs to the enclosing step.
6611        const FIELDS: &[&str] = &["subtree", "path", "query", "as"];
6612        self.field_ahead(FIELDS)
6613    }
6614
6615    /// `<one of names>` immediately followed by `:`.
6616    fn field_ahead(&self, names: &[&str]) -> bool {
6617        let cur = self.current();
6618        if !names.contains(&cur.value.as_str()) {
6619            return false;
6620        }
6621        self.tokens
6622            .get(self.pos + 1)
6623            .is_some_and(|t| t.ttype == TokenType::Colon)
6624    }
6625
6626    /// v2.83.0 — a CONFIG KEY: `"env:DATABASE_URL"` or the bare
6627    /// `env:DATABASE_URL` README publishes.
6628    ///
6629    /// v2.67.0 made `connection:`/`endpoint:` a config KEY rather than a URL or a
6630    /// DSN — the address resolves per deployment. README writes both the
6631    /// quoted and the bare spelling; the parser took only the quoted one, so
6632    /// every published `axonstore` with an unquoted key failed on its own
6633    /// third line. One value, two spellings — the epsilon/tolerance
6634    /// resolution of v2.83.0, applied to the config surface.
6635    fn parse_config_key(&mut self) -> Result<String, ParseError> {
6636        if self.check(TokenType::StringLit) {
6637            return Ok(self.consume(TokenType::StringLit)?.value.clone());
6638        }
6639        let scheme = self.consume_any_ident_or_kw()?.value.clone();
6640        if self.check(TokenType::Colon) {
6641            self.advance();
6642            let key = self.consume_any_ident_or_kw()?.value.clone();
6643            return Ok(format!("{scheme}:{key}"));
6644        }
6645        Ok(scheme)
6646    }
6647
6648    /// v2.83.0 — a PIX field value: a string literal OR a binding
6649    /// reference. README writes `query: question` (the flow parameter) far
6650    /// more often than a literal, and the parser accepted only the literal —
6651    /// which is why every published `navigate` failed on its own second line.
6652    fn parse_pix_value(&mut self) -> Result<String, ParseError> {
6653        if self.check(TokenType::StringLit) {
6654            return Ok(self.consume(TokenType::StringLit)?.value.clone());
6655        }
6656        Ok(self.consume_any_ident_or_kw()?.value.clone())
6657    }
6658
6659    fn parse_navigate_step(&mut self) -> Result<FlowStep, ParseError> {
6660        let tok = self.current().clone();
6661        self.advance();
6662        let pix_name = self.consume_any_ident_or_kw()?.value.clone();
6663        let mut node = NavigateStep {
6664            depth: None,
6665            pix_name,
6666            corpus_name: String::new(),
6667            query_expr: String::new(),
6668            trail_enabled: false,
6669            output_name: String::new(),
6670            seed: String::new(),
6671            budget: None,
6672            where_expr: String::new(),
6673            loc: Loc {
6674                line: tok.line,
6675                column: tok.column,
6676            },
6677        };
6678        // v2.83.0 — the BRACELESS field form, which is what README pix/
6679        // corpus publishes everywhere:
6680        //
6681        //     navigate ContractIndex
6682        //         query: question
6683        //         trail: enabled
6684        //         as: relevant_sections
6685        //
6686        // Terminated by the field-name set, not by a brace: the navigate
6687        // fields are a CLOSED catalog, so "the next token is one of these and
6688        // is followed by a colon" is an unambiguous continuation test. That is
6689        // the same closed-catalog discipline the rest of the language uses,
6690        // and it is why this form needs no delimiter to be parseable.
6691        if !self.check(TokenType::LBrace) {
6692            while self.at_navigate_field() {
6693                let f = self.current().value.clone();
6694                self.advance();
6695                self.consume(TokenType::Colon)?;
6696                match f.as_str() {
6697                    "corpus" => node.corpus_name = self.consume_any_ident_or_kw()?.value.clone(),
6698                    "query" => node.query_expr = self.parse_pix_value()?,
6699                    "trail" => {
6700                        let v = self.consume_any_ident_or_kw()?.value;
6701                        node.trail_enabled = matches!(v.as_str(), "true" | "enabled" | "on");
6702                    }
6703                    "output" | "as" => {
6704                        node.output_name = self.consume_any_ident_or_kw()?.value.clone()
6705                    }
6706                    "from" => node.seed = self.consume_any_ident_or_kw()?.value.clone(),
6707                    "budget" => node.budget = self.parse_optional_int(),
6708                    "where" => node.where_expr = self.parse_pix_value()?,
6709                    "depth" => node.depth = self.parse_optional_int(),
6710                    // v2.83.0 — `recall: episodic` selects the MDN memory
6711                    // mode README's clinical/legal examples write. The
6712                    // navigator's episodic path is v2.13.0's adaptive corpus
6713                    // reinforcement, keyed by the corpus declaration; the
6714                    // value is accepted and recorded on the seed so nothing
6715                    // is silently dropped, and the adaptive path already
6716                    // reads the corpus-level flag.
6717                    "recall" => {
6718                        let mode = self.consume_any_ident_or_kw()?.value.clone();
6719                        if node.seed.is_empty() {
6720                            node.seed = format!("recall:{mode}");
6721                        }
6722                    }
6723                    _ => self.skip_value(),
6724                }
6725            }
6726        }
6727        if self.check(TokenType::LBrace) {
6728            self.advance();
6729            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6730                let f = self.current().value.clone();
6731                self.advance();
6732                if self.check(TokenType::Colon) {
6733                    self.advance();
6734                    match f.as_str() {
6735                        "corpus" => {
6736                            node.corpus_name = self.consume_any_ident_or_kw()?.value.clone()
6737                        }
6738                        "query" => node.query_expr = self.parse_pix_value()?,
6739                        "trail" => {
6740                            let v = self.consume_any_ident_or_kw()?.value;
6741                            node.trail_enabled =
6742                                matches!(v.as_str(), "true" | "enabled" | "on");
6743                        }
6744                        "output" | "as" => {
6745                            node.output_name = self.consume_any_ident_or_kw()?.value.clone()
6746                        }
6747                        // v2.13.0 — MDN corpus-graph navigation.
6748                        "from" => node.seed = self.consume_any_ident_or_kw()?.value.clone(),
6749                        "budget" => node.budget = self.parse_optional_int(),
6750                        // v2.17.0 (Q2) — column-scoped navigation: a raw filter
6751                        // expr (mirrors `retrieve … where`) pushed to the SELECT
6752                        // that sources the corpus `documents:`/`relations:` rows,
6753                        // so a `corpus from axonstore` is scoped to a sub-tenant
6754                        // COLUMN (`where: "tenant_id == '${tenant_id}'"`), not just
6755                        // the axon-tenant RLS scope. Resolved by the v1.32.0 filter
6756                        // compiler at runtime (`${name}` → `$N` bind params).
6757                        "where" => {
6758                            node.where_expr = self.consume(TokenType::StringLit)?.value.clone()
6759                        }
6760                        _ => self.skip_value(),
6761                    }
6762                }
6763            }
6764            if self.check(TokenType::RBrace) {
6765                self.advance();
6766            }
6767        }
6768        Ok(FlowStep::Navigate(node))
6769    }
6770
6771    fn parse_drill_step(&mut self) -> Result<FlowStep, ParseError> {
6772        let tok = self.current().clone();
6773        self.advance();
6774        let pix_name = self.consume_any_ident_or_kw()?.value.clone();
6775        let mut node = DrillStep {
6776            pix_name,
6777            subtree_path: String::new(),
6778            query_expr: String::new(),
6779            output_name: String::new(),
6780            loc: Loc {
6781                line: tok.line,
6782                column: tok.column,
6783            },
6784        };
6785        // v2.83.0 — `drill <Ref> into "<path>" query: … as: …`, the form
6786        // README publishes. `into` is a positional keyword (no colon), the
6787        // rest is the same braceless closed-catalog field list as `navigate`.
6788        if self.current().value == "into" {
6789            self.advance();
6790            // v2.83.0 — README writes BOTH `into "Liabilities"` (a title)
6791            // and `into findings.top_region` (a dotted binding path). The
6792            // subtree path is dot-separated either way, so both spellings
6793            // land in the same field.
6794            node.subtree_path = if self.check(TokenType::StringLit) {
6795                self.consume(TokenType::StringLit)?.value.clone()
6796            } else {
6797                self.parse_dotted_identifier()?
6798            };
6799        }
6800        if !self.check(TokenType::LBrace) {
6801            while self.at_drill_field() {
6802                let f = self.current().value.clone();
6803                self.advance();
6804                self.consume(TokenType::Colon)?;
6805                match f.as_str() {
6806                    "subtree" | "path" => {
6807                        node.subtree_path = self.consume(TokenType::StringLit)?.value.clone()
6808                    }
6809                    "query" => node.query_expr = self.parse_pix_value()?,
6810                    "output" | "as" => {
6811                        node.output_name = self.consume_any_ident_or_kw()?.value.clone()
6812                    }
6813                    _ => self.skip_value(),
6814                }
6815            }
6816        }
6817        if self.check(TokenType::LBrace) {
6818            self.advance();
6819            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6820                let f = self.current().value.clone();
6821                self.advance();
6822                if self.check(TokenType::Colon) {
6823                    self.advance();
6824                    match f.as_str() {
6825                        "subtree" | "path" => {
6826                            node.subtree_path = self.consume(TokenType::StringLit)?.value.clone()
6827                        }
6828                        "query" => node.query_expr = self.parse_pix_value()?,
6829                        "output" | "as" => {
6830                            node.output_name = self.consume_any_ident_or_kw()?.value.clone()
6831                        }
6832                        _ => self.skip_value(),
6833                    }
6834                }
6835            }
6836            if self.check(TokenType::RBrace) {
6837                self.advance();
6838            }
6839        }
6840        Ok(FlowStep::Drill(node))
6841    }
6842
6843    fn parse_corroborate_step(&mut self) -> Result<FlowStep, ParseError> {
6844        let tok = self.current().clone();
6845        self.advance();
6846        let nav_ref = self.consume_any_ident_or_kw()?.value.clone();
6847        let mut output = String::new();
6848        if self.check(TokenType::Arrow) {
6849            self.advance();
6850            output = self.consume_any_ident_or_kw()?.value.clone();
6851        }
6852        Ok(FlowStep::Corroborate(CorroborateStep {
6853            navigate_ref: nav_ref,
6854            output_name: output,
6855            loc: Loc {
6856                line: tok.line,
6857                column: tok.column,
6858            },
6859        }))
6860    }
6861
6862    fn parse_listen_step(&mut self) -> Result<FlowStep, ParseError> {
6863        let tok = self.current().clone();
6864        self.advance();
6865        // v1.6.0 D4 — dual-mode listen:
6866        // • String topic (legacy, deprecated since v1.6.0)
6867        //   • Identifier (canonical: declared ChannelDefinition)
6868        let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
6869            (self.consume(TokenType::StringLit)?.value.clone(), false)
6870        } else {
6871            (self.consume_any_ident_or_kw()?.value.clone(), true)
6872        };
6873        let mut alias = String::new();
6874        if !self.at_declaration_start()
6875            && !self.check(TokenType::RBrace)
6876            && !self.check(TokenType::LBrace)
6877        {
6878            let next = self.current().clone();
6879            if next.value == "as" || next.ttype == TokenType::As {
6880                self.advance();
6881                alias = self.consume_any_ident_or_kw()?.value.clone();
6882            }
6883        }
6884        // v2.4.0 — parse the handler body into real flow-steps (was
6885        // `skip_braced_block`'d, leaving the listener inert). The body runs on
6886        // each event / scheduled tick.
6887        let body = self.parse_listener_body()?;
6888        Ok(FlowStep::Listen(ListenStep {
6889            channel,
6890            channel_is_ref,
6891            event_alias: alias,
6892            body,
6893            loc: Loc {
6894                line: tok.line,
6895                column: tok.column,
6896            },
6897        }))
6898    }
6899
6900    /// v2.4.0 — parse a `listen … { <flow steps> }` handler body. The body
6901    /// is OPTIONAL (a bodyless `listen channel` returns an empty Vec); when
6902    /// present, each statement is a real [`FlowStep`] (the same grammar as a
6903    /// flow / `quant` / `par` body), executed per trigger by the v2.4.0 runtime.
6904    fn parse_listener_body(&mut self) -> Result<Vec<FlowStep>, ParseError> {
6905        let mut body = Vec::new();
6906        if self.check(TokenType::LBrace) {
6907            self.advance(); // consume `{`
6908            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6909                body.push(self.parse_flow_step()?);
6910            }
6911            self.consume(TokenType::RBrace)?;
6912        }
6913        Ok(body)
6914    }
6915
6916    /// v2.83.0 — `retrieve [from] <Store> [where "<expr>"] [as <alias>]`
6917    /// alongside the pre-existing braced `retrieve <Store> { where: … as: … }`.
6918    ///
6919    /// README axonstore writes the braceless form with `from` and with `where`
6920    /// taking its argument DIRECTLY — no colon. Neither spelling parsed, so the
6921    /// only published `retrieve` failed on its own first line.
6922    fn parse_retrieve_step(&mut self) -> Result<FlowStep, ParseError> {
6923        let tok = self.current().clone();
6924        self.advance();
6925        // `from` is optional noise-with-meaning: it reads as English and the
6926        // store name carries the content either way.
6927        if self.check(TokenType::From) || self.current().value == "from" {
6928            self.advance();
6929        }
6930        let store = self.consume_any_ident_or_kw()?.value.clone();
6931        let mut where_expr = String::new();
6932        let mut alias = String::new();
6933        let mut order_by = String::new();
6934        let mut limit_expr = String::new();
6935        let mut aggregate = String::new();
6936        let mut group_by = String::new();
6937        let mut cache = String::new();
6938        // v2.83.0 — the BRACELESS clauses README publishes. Note they
6939        // take their argument with NO colon (`where "…"`, `as record`), which
6940        // is why the closed-catalog `field_ahead` test used elsewhere does not
6941        // apply: the terminator here is the clause keyword itself. Both names
6942        // are absent from the step-body field set, so a `retrieve` written
6943        // inside a step cannot swallow the step's own fields.
6944        loop {
6945            match self.current().value.as_str() {
6946                "where" if !self.check(TokenType::LBrace) => {
6947                    self.advance();
6948                    where_expr = self.consume(TokenType::StringLit)?.value.clone();
6949                }
6950                "as" => {
6951                    self.advance();
6952                    alias = self.consume_any_ident_or_kw()?.value.clone();
6953                }
6954                _ => break,
6955            }
6956        }
6957        if self.check(TokenType::LBrace) {
6958            self.advance();
6959            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6960                let f = self.current().value.clone();
6961                self.advance();
6962                if self.check(TokenType::Colon) {
6963                    self.advance();
6964                    match f.as_str() {
6965                        "where" => where_expr = self.consume(TokenType::StringLit)?.value.clone(),
6966                        "as" | "alias" => alias = self.consume_any_ident_or_kw()?.value.clone(),
6967                        // v2.21.0 — `order_by:` is a string literal
6968                        // (`"col asc, col2 desc"`), same surface as `where:`.
6969                        "order_by" => {
6970                            order_by = self.consume(TokenType::StringLit)?.value.clone()
6971                        }
6972                        // v2.21.0 — `limit:` is a bare integer literal
6973                        // (`limit: 100`) OR a string carrying a binding
6974                        // (`limit: "${max}"`). Captured raw; the runtime
6975                        // resolves + validates it as a `u32`.
6976                        "limit" => {
6977                            let t = self.current().clone();
6978                            match t.ttype {
6979                                TokenType::Integer | TokenType::StringLit => {
6980                                    limit_expr = t.value.clone();
6981                                    self.advance();
6982                                }
6983                                _ => self.skip_value(),
6984                            }
6985                        }
6986                        // v2.33.0 — `aggregate:` is a string literal from
6987                        // the CLOSED catalog (`"count"`, `"sum(tokens)"`, …);
6988                        // `group_by:` is a string literal listing columns
6989                        // (`"industry, status"`). Both captured raw; the
6990                        // v1.31.0 proof (axon-T843/T844/T845) + the runtime
6991                        // (`filter::parse_aggregate_clause`) validate.
6992                        "aggregate" => {
6993                            aggregate = self.consume(TokenType::StringLit)?.value.clone()
6994                        }
6995                        "group_by" => {
6996                            group_by = self.consume(TokenType::StringLit)?.value.clone()
6997                        }
6998                        // v2.40.0 — `cache:` names a declared `cache`
6999                        // policy. A retrieve reads a store (never `pure`), so
7000                        // caching it always accepts staleness — the checker
7001                        // requires a finite `ttl:` on the referenced cache
7002                        // (axon-T865) and resolves the reference (axon-T864).
7003                        "cache" => cache = self.consume_any_ident_or_kw()?.value.clone(),
7004                        _ => self.skip_value(),
7005                    }
7006                }
7007            }
7008            if self.check(TokenType::RBrace) {
7009                self.advance();
7010            }
7011        }
7012        Ok(FlowStep::Retrieve(RetrieveStep {
7013            store_name: store,
7014            where_expr,
7015            alias,
7016            order_by,
7017            limit_expr,
7018            aggregate,
7019            group_by,
7020            cache,
7021            loc: Loc {
7022                line: tok.line,
7023                column: tok.column,
7024            },
7025        }))
7026    }
7027
7028    /// v1.30.0 — Parse a `purge` step, capturing the optional
7029    /// `{ where: "<expr>" }` filter. (v1.30.0 moved `mutate` to its
7030    /// own `parse_mutate_step`, which also captures SET columns; this
7031    /// helper now serves `purge` alone — a `DELETE` has no SET clause.)
7032    ///
7033    /// Before v1.30.0 these two steps parsed via `parse_flow_step_simple`,
7034    /// which *skipped* the braced block — so a written `where:` clause
7035    /// was silently dropped and every `mutate`/`purge` ran against the
7036    /// whole store, leaving the entire v1.30.0 parameterized-filter
7037    /// machinery unreachable for them. This mirror of `parse_retrieve_step`
7038    /// (minus the `as:` alias — a mutate/purge binds no result) closes
7039    /// that gap. Returns `(loc, store_name, where_expr)`.
7040    fn parse_store_where_step(
7041        &mut self,
7042    ) -> Result<(Loc, String, String), ParseError> {
7043        let tok = self.current().clone();
7044        self.advance(); // consume the keyword
7045        let store = if self.at_declaration_start()
7046            || self.check(TokenType::RBrace)
7047            || self.check(TokenType::Eof)
7048        {
7049            String::new()
7050        } else {
7051            self.consume_any_ident_or_kw()?.value.clone()
7052        };
7053        let mut where_expr = String::new();
7054        if self.check(TokenType::LBrace) {
7055            self.advance();
7056            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7057                let field = self.current().value.clone();
7058                self.advance();
7059                if self.check(TokenType::Colon) {
7060                    self.advance();
7061                    match field.as_str() {
7062                        "where" => {
7063                            where_expr =
7064                                self.consume(TokenType::StringLit)?.value.clone()
7065                        }
7066                        _ => self.skip_value(),
7067                    }
7068                }
7069            }
7070            if self.check(TokenType::RBrace) {
7071                self.advance();
7072            }
7073        }
7074        Ok((
7075            Loc {
7076                line: tok.line,
7077                column: tok.column,
7078            },
7079            store,
7080            where_expr,
7081        ))
7082    }
7083
7084    /// v1.30.0 — Parse a `persist` step, capturing the optional
7085    /// `{ col: value }` field block.
7086    ///
7087    /// Before v1.30.0 `persist` parsed via `parse_flow_step_simple`,
7088    /// which *skipped* the braced block — so a written field block was
7089    /// silently dropped and the runtime fell back to writing every
7090    /// context binding as a row, which fails against any real table
7091    /// (flows always carry more bindings than a table has columns).
7092    /// This captures the declared columns into `PersistStep.fields`;
7093    /// the runtime writes exactly those (interpolated). A `persist`
7094    /// with no block keeps the v1.30.0 user-bindings fallback — fully
7095    /// backward-compatible. Mirror of `parse_retrieve_step`, but the
7096    /// keys are arbitrary column names rather than the fixed
7097    /// `where:` / `as:` filter keys.
7098    ///
7099    /// The optional `into` connector (`persist into <store>`) is
7100    /// accepted and skipped — before v1.30.0 `into` was captured as
7101    /// the store name.
7102    fn parse_persist_step(&mut self) -> Result<FlowStep, ParseError> {
7103        let tok = self.current().clone();
7104        self.advance(); // consume `persist`
7105        // Optional `into` connector — skip it so the store name that
7106        // follows is not mistaken for the target.
7107        if self.current().value == "into" && !self.check(TokenType::LBrace) {
7108            self.advance();
7109        }
7110        let store = if self.at_declaration_start()
7111            || self.check(TokenType::LBrace)
7112            || self.check(TokenType::RBrace)
7113            || self.check(TokenType::Eof)
7114        {
7115            String::new()
7116        } else {
7117            self.consume_any_ident_or_kw()?.value.clone()
7118        };
7119        let mut fields: Vec<(String, String)> = Vec::new();
7120        if self.check(TokenType::LBrace) {
7121            self.advance();
7122            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7123                let col = self.current().value.clone();
7124                self.advance();
7125                if self.check(TokenType::Colon) {
7126                    self.advance();
7127                    let value = if self.check(TokenType::StringLit) {
7128                        self.consume(TokenType::StringLit)?.value.clone()
7129                    } else if self.check(TokenType::RBrace)
7130                        || self.check(TokenType::Eof)
7131                        || self.check(TokenType::Colon)
7132                    {
7133                        String::new()
7134                    } else {
7135                        let v = self.current().clone();
7136                        self.advance();
7137                        v.value.clone()
7138                    };
7139                    fields.push((col, value));
7140                }
7141            }
7142            if self.check(TokenType::RBrace) {
7143                self.advance();
7144            }
7145        }
7146        Ok(FlowStep::Persist(PersistStep {
7147            store_name: store,
7148            fields,
7149            loc: Loc {
7150                line: tok.line,
7151                column: tok.column,
7152            },
7153        }))
7154    }
7155
7156    /// v1.30.0 — Parse a `mutate` step, capturing both the
7157    /// `{ where: "<expr>" }` filter AND the `{ col: value }` SET
7158    /// assignments.
7159    ///
7160    /// Before v1.30.0 `mutate` parsed via `parse_store_where_step`,
7161    /// which captured only `where:` and *skipped* every other key — so
7162    /// the runtime built the `UPDATE … SET` clause from every flow
7163    /// binding (params + step results + `let`s), which fails against
7164    /// any real table (`column "X" does not exist`). This closes the
7165    /// gap symmetrically to 35.o's `persist` block: every key other
7166    /// than `where:` is a SET column; a `mutate` with no SET column
7167    /// keeps the v1.31.0 user-bindings fallback. `where:` keeps its
7168    /// string-literal grammar (as in `retrieve` / `purge`).
7169    fn parse_mutate_step(&mut self) -> Result<FlowStep, ParseError> {
7170        let tok = self.current().clone();
7171        self.advance(); // consume `mutate`
7172        let store = if self.at_declaration_start()
7173            || self.check(TokenType::LBrace)
7174            || self.check(TokenType::RBrace)
7175            || self.check(TokenType::Eof)
7176        {
7177            String::new()
7178        } else {
7179            self.consume_any_ident_or_kw()?.value.clone()
7180        };
7181        let mut where_expr = String::new();
7182        let mut fields: Vec<(String, String)> = Vec::new();
7183        if self.check(TokenType::LBrace) {
7184            self.advance();
7185            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7186                let key = self.current().value.clone();
7187                self.advance();
7188                if self.check(TokenType::Colon) {
7189                    self.advance();
7190                    if key == "where" {
7191                        where_expr =
7192                            self.consume(TokenType::StringLit)?.value.clone();
7193                    } else {
7194                        let value = if self.check(TokenType::StringLit) {
7195                            self.consume(TokenType::StringLit)?.value.clone()
7196                        } else if self.check(TokenType::RBrace)
7197                            || self.check(TokenType::Eof)
7198                            || self.check(TokenType::Colon)
7199                        {
7200                            String::new()
7201                        } else {
7202                            let v = self.current().clone();
7203                            self.advance();
7204                            v.value.clone()
7205                        };
7206                        fields.push((key, value));
7207                    }
7208                }
7209            }
7210            if self.check(TokenType::RBrace) {
7211                self.advance();
7212            }
7213        }
7214        Ok(FlowStep::Mutate(MutateStep {
7215            store_name: store,
7216            where_expr,
7217            fields,
7218            loc: Loc {
7219                line: tok.line,
7220                column: tok.column,
7221            },
7222        }))
7223    }
7224
7225    // ── TIER 2 DECLARATIONS ────────────────────────────────────────
7226
7227    fn parse_agent(&mut self) -> Result<AgentDefinition, ParseError> {
7228        let tok = self.consume(TokenType::Agent)?;
7229        let name = self.consume(TokenType::Identifier)?.value;
7230        let mut node = AgentDefinition {
7231            name,
7232            goal: String::new(),
7233            tools: Vec::new(),
7234            memory_ref: String::new(),
7235            strategy: String::new(),
7236            on_stuck: String::new(),
7237            shield_ref: String::new(),
7238            max_iterations: None,
7239            max_tokens: None,
7240            max_time: String::new(),
7241            max_cost: None,
7242            return_type: String::new(),
7243            body: 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        // Optional signature position: `agent Name(params…) -> T {`. The
7252        // parameter list is accepted and not modelled (an agent takes its input
7253        // from the call site); the return type IS modelled — it used to be
7254        // skipped here, which is how `return:` became a promise the README made
7255        // and nothing read.
7256        if self.check(TokenType::LParen) {
7257            let mut depth = 0usize;
7258            while !self.check(TokenType::Eof) {
7259                if self.check(TokenType::LParen) {
7260                    depth += 1;
7261                } else if self.check(TokenType::RParen) {
7262                    depth -= 1;
7263                    if depth == 0 {
7264                        self.advance();
7265                        break;
7266                    }
7267                }
7268                self.advance();
7269            }
7270        }
7271        if self.check(TokenType::Arrow) {
7272            self.advance();
7273            node.return_type = self.parse_output_type_string()?;
7274        }
7275        self.consume(TokenType::LBrace)?;
7276        // The block is a CLOSED catalogue. An unknown field used to be skipped
7277        // in silence, so a typo (`max_iteration: 6`) parsed clean and the agent
7278        // ran unbounded until the dispatcher refused it — the opposite of what
7279        // a type error is for. Every field the runtime reads is listed here;
7280        // `step … { … }` blocks form the `custom` policy's body.
7281        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7282            if self.check(TokenType::Step) {
7283                let step = self.parse_step()?;
7284                node.body.push(step);
7285                continue;
7286            }
7287            let field = self.current().clone();
7288            let field_name = field.value.clone();
7289            self.advance();
7290            if !self.check(TokenType::Colon) {
7291                return Err(self.error(&format!(
7292                    "unexpected `{field_name}` inside `agent {}` — an agent block holds \
7293                     `field: value` pairs and `step Name {{ … }}` blocks; valid fields: \
7294                     goal, tools, memory, strategy, on_stuck, shield, max_iterations, \
7295                     max_tokens, max_time, max_cost, return",
7296                    node.name
7297                )));
7298            }
7299            self.advance();
7300            match field_name.as_str() {
7301                "goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
7302                "tools" => node.tools = self.parse_bracketed_identifiers()?,
7303                "memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
7304                "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
7305                "on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
7306                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
7307                "max_iterations" => node.max_iterations = self.parse_optional_int(),
7308                "max_tokens" => node.max_tokens = self.parse_optional_int(),
7309                "max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
7310                "max_cost" => node.max_cost = self.parse_optional_float(),
7311                "return" => node.return_type = self.parse_output_type_string()?,
7312                other => {
7313                    return Err(self.error(&format!(
7314                        "unknown agent field `{other}` in `agent {}` — the agent block is a \
7315                         closed catalog; valid fields: goal, tools, memory, strategy, \
7316                         on_stuck, shield, max_iterations, max_tokens, max_time, max_cost, \
7317                         return (plus `step Name {{ … }}` blocks for `strategy: custom`)",
7318                        node.name
7319                    )));
7320                }
7321            }
7322        }
7323        self.consume(TokenType::RBrace)?;
7324        Ok(node)
7325    }
7326
7327    /// v2.5.0 — `extension Name { category: effects|scan, members: [ … ] }`.
7328    /// The parser is permissive on field/category VALUES (validated in
7329    /// v2.5.0 by the type-checker — no-shadowing, category-membership);
7330    /// it only enforces the structural grammar here.
7331    fn parse_extension(&mut self) -> Result<ExtensionDefinition, ParseError> {
7332        let tok = self.consume(TokenType::Extension)?;
7333        let name = self.consume(TokenType::Identifier)?.value;
7334        let mut node = ExtensionDefinition {
7335            name,
7336            category: String::new(),
7337            members: Vec::new(),
7338            loc: Loc {
7339                line: tok.line,
7340                column: tok.column,
7341            },
7342            leading_trivia: Vec::new(),
7343            trailing_trivia: Vec::new(),
7344        };
7345        self.consume(TokenType::LBrace)?;
7346        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7347            let field_name = self.current().value.clone();
7348            self.advance();
7349            if self.check(TokenType::Colon) {
7350                self.advance();
7351                match field_name.as_str() {
7352                    "category" => {
7353                        node.category = self.consume_any_ident_or_kw()?.value.clone()
7354                    }
7355                    "members" => node.members = self.parse_extension_members()?,
7356                    _ => self.skip_value(),
7357                }
7358            } else if self.check(TokenType::LBrace) {
7359                self.skip_braced_block()?;
7360            }
7361        }
7362        self.consume(TokenType::RBrace)?;
7363        Ok(node)
7364    }
7365
7366    /// v2.5.0 — parse `[ "name" [: { semantics: "…", default_confidence: 0.8 } ], … ]`.
7367    /// Each member is a string literal optionally followed by a metadata
7368    /// block. Trailing/interleaved commas are tolerated.
7369    fn parse_extension_members(&mut self) -> Result<Vec<ExtensionMember>, ParseError> {
7370        let mut members = Vec::new();
7371        self.consume(TokenType::LBracket)?;
7372        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
7373            let name_tok = self.consume(TokenType::StringLit)?;
7374            let mut member = ExtensionMember {
7375                name: name_tok.value.clone(),
7376                semantics: None,
7377                default_confidence: None,
7378                loc: Loc {
7379                    line: name_tok.line,
7380                    column: name_tok.column,
7381                },
7382            };
7383            // Optional `: { semantics: "…", default_confidence: 0.8 }`.
7384            if self.check(TokenType::Colon) {
7385                self.advance();
7386                self.consume(TokenType::LBrace)?;
7387                while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7388                    let mkey = self.current().value.clone();
7389                    self.advance();
7390                    if self.check(TokenType::Colon) {
7391                        self.advance();
7392                        match mkey.as_str() {
7393                            "semantics" => {
7394                                member.semantics =
7395                                    Some(self.consume(TokenType::StringLit)?.value.clone())
7396                            }
7397                            "default_confidence" => {
7398                                member.default_confidence = self.parse_optional_float()
7399                            }
7400                            _ => self.skip_value(),
7401                        }
7402                    }
7403                    if self.check(TokenType::Comma) {
7404                        self.advance();
7405                    }
7406                }
7407                self.consume(TokenType::RBrace)?;
7408            }
7409            members.push(member);
7410            if self.check(TokenType::Comma) {
7411                self.advance();
7412            }
7413        }
7414        self.consume(TokenType::RBracket)?;
7415        Ok(members)
7416    }
7417
7418    /// v2.27.0 — `window <Name> { timezone: "…" allow: [ {days hours} ]
7419    /// exclude: [ "YYYY-MM-DD", … ]  on_outside: skip|defer|warn }`.
7420    fn parse_window(&mut self) -> Result<WindowDefinition, ParseError> {
7421        let tok = self.consume(TokenType::Window)?;
7422        let name = self.consume(TokenType::Identifier)?.value;
7423        let mut node = WindowDefinition {
7424            name,
7425            timezone: String::new(),
7426            allow: Vec::new(),
7427            exclude: Vec::new(),
7428            on_outside: String::new(),
7429            loc: Loc {
7430                line: tok.line,
7431                column: tok.column,
7432            },
7433            leading_trivia: Vec::new(),
7434            trailing_trivia: Vec::new(),
7435        };
7436        self.consume(TokenType::LBrace)?;
7437        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7438            let field_name = self.consume_any_ident_or_kw()?.value;
7439            self.consume(TokenType::Colon)?;
7440            match field_name.as_str() {
7441                "timezone" => node.timezone = self.consume(TokenType::StringLit)?.value,
7442                "allow" => node.allow = self.parse_window_allow()?,
7443                "exclude" => node.exclude = self.parse_window_exclude()?,
7444                "on_outside" => node.on_outside = self.consume_any_ident_or_kw()?.value,
7445                _ => self.skip_value(),
7446            }
7447        }
7448        self.consume(TokenType::RBrace)?;
7449        Ok(node)
7450    }
7451
7452    /// v2.27.0 — the `allow: [ { … }, { … } ]` span list.
7453    fn parse_window_allow(&mut self) -> Result<Vec<WindowSpan>, ParseError> {
7454        self.consume(TokenType::LBracket)?;
7455        let mut spans = Vec::new();
7456        if !self.check(TokenType::RBracket) {
7457            spans.push(self.parse_window_span()?);
7458            while self.check(TokenType::Comma) {
7459                self.advance();
7460                if self.check(TokenType::RBracket) {
7461                    break; // trailing comma
7462                }
7463                spans.push(self.parse_window_span()?);
7464            }
7465        }
7466        self.consume(TokenType::RBracket)?;
7467        Ok(spans)
7468    }
7469
7470    /// v2.27.0 — the `exclude: [ "YYYY-MM-DD", … ]` holiday list (ISO
7471    /// date-string literals; validated for real-calendar-date-ness by the
7472    /// `axon-T826` type check). An empty list / absent field ⇒ no holidays.
7473    fn parse_window_exclude(&mut self) -> Result<Vec<String>, ParseError> {
7474        self.consume(TokenType::LBracket)?;
7475        let mut dates = Vec::new();
7476        if !self.check(TokenType::RBracket) {
7477            dates.push(self.consume(TokenType::StringLit)?.value);
7478            while self.check(TokenType::Comma) {
7479                self.advance();
7480                if self.check(TokenType::RBracket) {
7481                    break; // trailing comma
7482                }
7483                dates.push(self.consume(TokenType::StringLit)?.value);
7484            }
7485        }
7486        self.consume(TokenType::RBracket)?;
7487        Ok(dates)
7488    }
7489
7490    /// v2.27.0 — one span `{ days: Mon..Fri hours: 9..18 }`.
7491    fn parse_window_span(&mut self) -> Result<WindowSpan, ParseError> {
7492        let tok = self.consume(TokenType::LBrace)?;
7493        let mut span = WindowSpan {
7494            day_start: String::new(),
7495            day_end: String::new(),
7496            hour_start: 0,
7497            hour_end: 0,
7498            loc: Loc {
7499                line: tok.line,
7500                column: tok.column,
7501            },
7502        };
7503        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7504            let field = self.consume_any_ident_or_kw()?.value;
7505            self.consume(TokenType::Colon)?;
7506            match field.as_str() {
7507                "days" => {
7508                    span.day_start = self.consume_any_ident_or_kw()?.value;
7509                    self.consume(TokenType::DotDot)?;
7510                    span.day_end = self.consume_any_ident_or_kw()?.value;
7511                }
7512                "hours" => {
7513                    span.hour_start = self.consume_number()? as i64;
7514                    self.consume(TokenType::DotDot)?;
7515                    span.hour_end = self.consume_number()? as i64;
7516                }
7517                _ => self.skip_value(),
7518            }
7519            if self.check(TokenType::Comma) {
7520                self.advance();
7521            }
7522        }
7523        self.consume(TokenType::RBrace)?;
7524        Ok(span)
7525    }
7526
7527    /// v4.5.0 — `attest <Name> { for:, basis:, residual:, by:, on: }`.
7528    ///
7529    /// The parser is deliberately permissive here and the checker is not.
7530    /// Every field is optional to PARSE, so that a half-written attestation
7531    /// still produces an AST the checker can complain about precisely —
7532    /// "this attestation has no signer" beats "expected `by`", which tells
7533    /// the author about the grammar instead of about the obligation.
7534    fn parse_attest(&mut self) -> Result<crate::ast::AttestDefinition, ParseError> {
7535        let tok = self.consume(TokenType::Attest)?;
7536        let name = self.consume(TokenType::Identifier)?.value;
7537        let mut node = crate::ast::AttestDefinition {
7538            name,
7539            loc: Loc {
7540                line: tok.line,
7541                column: tok.column,
7542            },
7543            ..Default::default()
7544        };
7545        self.consume(TokenType::LBrace)?;
7546        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7547            let field_name = self.current().value.clone();
7548            let field_loc = Loc {
7549                line: self.current().line,
7550                column: self.current().column,
7551            };
7552            self.advance();
7553            if self.check(TokenType::Colon) {
7554                self.advance();
7555                match field_name.as_str() {
7556                    // `for` is a keyword elsewhere in the grammar, which is why
7557                    // the field name is read as text rather than matched as a
7558                    // token — the same shape every other block here uses.
7559                    "for" => node.for_type = self.consume_any_ident_or_kw()?.value.clone(),
7560                    "basis" => node.basis = self.consume_any_ident_or_kw()?.value.clone(),
7561                    "residual" => node.residual = self.parse_bracketed_identifiers()?,
7562                    "by" => node.by = self.consume(TokenType::StringLit)?.value.clone(),
7563                    "on" => node.on = self.consume(TokenType::StringLit)?.value.clone(),
7564                    _ => {
7565                        node.unknown_fields.push((field_name.clone(), field_loc));
7566                        self.skip_value()
7567                    }
7568                }
7569            } else if self.check(TokenType::LBrace) {
7570                self.skip_braced_block()?;
7571            }
7572        }
7573        self.consume(TokenType::RBrace)?;
7574        Ok(node)
7575    }
7576
7577    fn parse_shield(&mut self) -> Result<ShieldDefinition, ParseError> {
7578        let tok = self.consume(TokenType::Shield)?;
7579        let name = self.consume(TokenType::Identifier)?.value;
7580        let mut node = ShieldDefinition {
7581            suppress: Vec::new(),
7582            generalise: Vec::new(),
7583            declassifies: Vec::new(),
7584            name,
7585            scan: Vec::new(),
7586            strategy: String::new(),
7587            on_breach: String::new(),
7588            severity: String::new(),
7589            quarantine: String::new(),
7590            max_retries: None,
7591            confidence_threshold: None,
7592            allow_tools: Vec::new(),
7593            deny_tools: Vec::new(),
7594            sandbox: None,
7595            redact: Vec::new(),
7596            log: String::new(),
7597            deflect_message: String::new(),
7598            taint: String::new(),
7599            compliance: Vec::new(),
7600            sign: String::new(),
7601            unknown_fields: Vec::new(),
7602            loc: Loc {
7603                line: tok.line,
7604                column: tok.column,
7605            },
7606            leading_trivia: Vec::new(),
7607            trailing_trivia: Vec::new(),
7608        };
7609        self.consume(TokenType::LBrace)?;
7610        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7611            let field_name = self.current().value.clone();
7612            let field_loc = Loc {
7613                line: self.current().line,
7614                column: self.current().column,
7615            };
7616            self.advance();
7617            if self.check(TokenType::Colon) {
7618                self.advance();
7619                match field_name.as_str() {
7620                    "scan" => node.scan = self.parse_bracketed_identifiers()?,
7621                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
7622                    "on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
7623                    "severity" => node.severity = self.consume_any_ident_or_kw()?.value.clone(),
7624                    "quarantine" => {
7625                        node.quarantine = self.consume(TokenType::StringLit)?.value.clone()
7626                    }
7627                    "max_retries" => node.max_retries = self.parse_optional_int(),
7628                    "confidence_threshold" => {
7629                        node.confidence_threshold = self.parse_optional_float()
7630                    }
7631                    "allow_tools" => node.allow_tools = self.parse_bracketed_identifiers()?,
7632                    "deny_tools" => node.deny_tools = self.parse_bracketed_identifiers()?,
7633                    "sandbox" => {
7634                        node.sandbox = Some(self.consume_any_ident_or_kw()?.value == "true")
7635                    }
7636                    "redact" => node.redact = self.parse_bracketed_identifiers()?,
7637                    // v4.5.0 — the classes this control may RETIRE. Empty for
7638                    // almost every shield: scanning is not declassifying.
7639                    "declassifies" => node.declassifies = self.parse_bracketed_identifiers()?,
7640                    "suppress" => node.suppress = self.parse_bracketed_identifiers()?,
7641                    "generalise" => node.generalise = self.parse_bracketed_identifiers()?,
7642                    "log" => node.log = self.consume_any_ident_or_kw()?.value.clone(),
7643                    "deflect_message" => {
7644                        node.deflect_message = self.consume(TokenType::StringLit)?.value.clone()
7645                    }
7646                    "taint" => node.taint = self.consume_any_ident_or_kw()?.value.clone(),
7647                    // ESK — covered regulatory classes.
7648                    "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
7649                    // v2.34.0 — egress signing algorithm (closed catalog,
7650                    // validated by the checker: `axon-T846`).
7651                    "sign" => node.sign = self.consume_any_ident_or_kw()?.value.clone(),
7652                    // v2.34.0 — the value is still skipped (leniency
7653                    // preserved) but the NAME is recorded so the checker
7654                    // emits `axon-W010` instead of a silent drop.
7655                    _ => {
7656                        node.unknown_fields.push((field_name.clone(), field_loc));
7657                        self.skip_value()
7658                    }
7659                }
7660            } else if self.check(TokenType::LBrace) {
7661                self.skip_braced_block()?;
7662            }
7663        }
7664        self.consume(TokenType::RBrace)?;
7665        Ok(node)
7666    }
7667
7668    fn parse_pix(&mut self) -> Result<PixDefinition, ParseError> {
7669        let tok = self.consume(TokenType::Pix)?;
7670        let name = self.consume(TokenType::Identifier)?.value;
7671        let mut node = PixDefinition {
7672            name,
7673            source: String::new(),
7674            depth: None,
7675            branching: None,
7676            model: String::new(),
7677            loc: Loc {
7678                line: tok.line,
7679                column: tok.column,
7680            },
7681            leading_trivia: Vec::new(),
7682            trailing_trivia: Vec::new(),
7683        };
7684        self.consume(TokenType::LBrace)?;
7685        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7686            let field_name = self.current().value.clone();
7687            self.advance();
7688            if self.check(TokenType::Colon) {
7689                self.advance();
7690                match field_name.as_str() {
7691                    "source" => node.source = self.consume(TokenType::StringLit)?.value.clone(),
7692                    "depth" => node.depth = self.parse_optional_int(),
7693                    "branching" => node.branching = self.parse_optional_int(),
7694                    "model" => node.model = self.consume_any_ident_or_kw()?.value.clone(),
7695                    _ => self.skip_value(),
7696                }
7697            } else if self.check(TokenType::LBrace) {
7698                self.skip_braced_block()?;
7699            }
7700        }
7701        self.consume(TokenType::RBrace)?;
7702        Ok(node)
7703    }
7704
7705    /// v2.12.0 — `ledger <Name> { source, depth, branching, model }`.
7706    /// The append-only audit chain (formerly the Provenance-Index reading of
7707    /// `pix`). Field grammar mirrors `pix` (same shape) but the SEMANTICS are
7708    /// audit, not navigation: `depth` = chain retention, `branching` = Merkle
7709    /// factor, `model` = hash slug (sha256 / blake3 / sha3).
7710    fn parse_ledger(&mut self) -> Result<LedgerDefinition, ParseError> {
7711        let tok = self.consume(TokenType::Ledger)?;
7712        let name = self.consume(TokenType::Identifier)?.value;
7713        let mut node = LedgerDefinition {
7714            name,
7715            source: String::new(),
7716            depth: None,
7717            branching: None,
7718            model: String::new(),
7719            loc: Loc {
7720                line: tok.line,
7721                column: tok.column,
7722            },
7723            leading_trivia: Vec::new(),
7724            trailing_trivia: Vec::new(),
7725        };
7726        self.consume(TokenType::LBrace)?;
7727        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7728            let field_name = self.current().value.clone();
7729            self.advance();
7730            if self.check(TokenType::Colon) {
7731                self.advance();
7732                match field_name.as_str() {
7733                    "source" => node.source = self.consume(TokenType::StringLit)?.value.clone(),
7734                    "depth" => node.depth = self.parse_optional_int(),
7735                    "branching" => node.branching = self.parse_optional_int(),
7736                    "model" => node.model = self.consume_any_ident_or_kw()?.value.clone(),
7737                    _ => self.skip_value(),
7738                }
7739            } else if self.check(TokenType::LBrace) {
7740                self.skip_braced_block()?;
7741            }
7742        }
7743        self.consume(TokenType::RBrace)?;
7744        Ok(node)
7745    }
7746
7747    fn parse_psyche(&mut self) -> Result<PsycheDefinition, ParseError> {
7748        let tok = self.consume(TokenType::Psyche)?;
7749        let name = self.consume(TokenType::Identifier)?.value;
7750        let mut node = PsycheDefinition {
7751            name,
7752            dimensions: Vec::new(),
7753            manifold_noise: None,
7754            manifold_momentum: None,
7755            safety_constraints: Vec::new(),
7756            quantum_enabled: None,
7757            inference_mode: String::new(),
7758            loc: Loc {
7759                line: tok.line,
7760                column: tok.column,
7761            },
7762            leading_trivia: Vec::new(),
7763            trailing_trivia: Vec::new(),
7764        };
7765        self.consume(TokenType::LBrace)?;
7766        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7767            let field_name = self.current().value.clone();
7768            self.advance();
7769            if self.check(TokenType::Colon) {
7770                self.advance();
7771                match field_name.as_str() {
7772                    "dimensions" => node.dimensions = self.parse_bracketed_identifiers()?,
7773                    "manifold_noise" => node.manifold_noise = self.parse_optional_float(),
7774                    "manifold_momentum" => node.manifold_momentum = self.parse_optional_float(),
7775                    // v2.83.0 — `safety:` is what README psyche publishes;
7776                    // `safety_constraints:` is what the parser has always taken.
7777                    // One field, two spellings — the `epsilon`/`tolerance`
7778                    // resolution of v2.83.0.
7779                    "safety_constraints" | "safety" => {
7780                        node.safety_constraints = self.parse_bracketed_identifiers()?
7781                    }
7782                    "quantum_enabled" => {
7783                        node.quantum_enabled = Some(self.consume_any_ident_or_kw()?.value == "true")
7784                    }
7785                    "inference_mode" => {
7786                        node.inference_mode = self.consume_any_ident_or_kw()?.value.clone()
7787                    }
7788                    _ => self.skip_value(),
7789                }
7790            } else if self.check(TokenType::LBrace) {
7791                self.skip_braced_block()?;
7792            }
7793        }
7794        self.consume(TokenType::RBrace)?;
7795        Ok(node)
7796    }
7797
7798    fn parse_corpus(&mut self) -> Result<CorpusDefinition, ParseError> {
7799        let tok = self.consume(TokenType::Corpus)?;
7800        let name = self.consume(TokenType::Identifier)?.value;
7801        let mut node = CorpusDefinition {
7802            name,
7803            documents: Vec::new(),
7804            relations: Vec::new(),
7805            adaptive: false,
7806            mcp_server: String::new(),
7807            mcp_resource_uri: String::new(),
7808            store_source: None,
7809            loc: Loc {
7810                line: tok.line,
7811                column: tok.column,
7812            },
7813            leading_trivia: Vec::new(),
7814            trailing_trivia: Vec::new(),
7815        };
7816        // corpus Name from mcp("server", "uri")  — static MCP-bound short form.
7817        // corpus Name from axonstore { documents: S(id,title)  relations: … }  —
7818        // v2.14.0 dynamic store-sourced MDN graph (falls through to the body).
7819        let mut dynamic = false;
7820        if self.check(TokenType::From) {
7821            self.advance();
7822            if self.check(TokenType::AxonStore) {
7823                self.advance();
7824                dynamic = true;
7825            } else {
7826                self.consume(TokenType::Mcp)?;
7827                self.consume(TokenType::LParen)?;
7828                node.mcp_server = self.consume(TokenType::StringLit)?.value.clone();
7829                self.consume(TokenType::Comma)?;
7830                node.mcp_resource_uri = self.consume(TokenType::StringLit)?.value.clone();
7831                self.consume(TokenType::RParen)?;
7832                return Ok(node);
7833            }
7834        }
7835        self.consume(TokenType::LBrace)?;
7836        // v2.14.0 — accumulate the store-mapping pieces while the dynamic body
7837        // is parsed; folded into `node.store_source` after the closing brace.
7838        let mut src = CorpusStoreSource {
7839            doc_store: String::new(),
7840            doc_id_col: String::new(),
7841            doc_title_col: String::new(),
7842            edge_store: String::new(),
7843            edge_from_col: String::new(),
7844            edge_to_col: String::new(),
7845            edge_type_col: String::new(),
7846            edge_weight_col: String::new(),
7847            loc: Loc {
7848                line: tok.line,
7849                column: tok.column,
7850            },
7851        };
7852        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7853            let field_name = self.current().value.clone();
7854            self.advance();
7855            if self.check(TokenType::Colon) {
7856                self.advance();
7857                match field_name.as_str() {
7858                    // v2.14.0 — dynamic: `documents: <DocStore>(id_col, title_col)`.
7859                    "documents" if dynamic => {
7860                        let (store, cols) = self.parse_corpus_store_mapping(2)?;
7861                        src.doc_store = store;
7862                        src.doc_id_col = cols[0].clone();
7863                        src.doc_title_col = cols[1].clone();
7864                    }
7865                    "documents" => node.documents = self.parse_bracketed_identifiers()?,
7866                    // v2.14.0 — dynamic: `relations: <EdgeStore>(from, to, etype, weight)`.
7867                    "relations" if dynamic => {
7868                        let (store, cols) = self.parse_corpus_store_mapping(4)?;
7869                        src.edge_store = store;
7870                        src.edge_from_col = cols[0].clone();
7871                        src.edge_to_col = cols[1].clone();
7872                        src.edge_type_col = cols[2].clone();
7873                        src.edge_weight_col = cols[3].clone();
7874                    }
7875                    // v2.13.0 — static typed weighted edges → MDN corpus graph.
7876                    "relations" => node.relations = self.parse_corpus_relations()?,
7877                    // v2.13.0 — enable the memory endofunctor.
7878                    "adaptive" => node.adaptive = self.consume_any_ident_or_kw()?.value == "true",
7879                    _ => self.skip_value(),
7880                }
7881            } else if self.check(TokenType::LBrace) {
7882                self.skip_braced_block()?;
7883            }
7884        }
7885        self.consume(TokenType::RBrace)?;
7886        if dynamic {
7887            node.store_source = Some(src);
7888        }
7889        Ok(node)
7890    }
7891
7892    /// v2.14.0 — parse a store-mapping `<StoreName>(col1, col2, …)` of exactly
7893    /// `n` columns. Used by the dynamic store-sourced corpus's `documents:` (2
7894    /// cols: id, title) and `relations:` (4 cols: from, to, etype, weight). The
7895    /// store name is an identifier (a declared `axonstore`); the columns may be
7896    /// keywords (a column could be named `from`/`type`), so they use the
7897    /// keyword-tolerant consumer. The type-checker validates store + columns.
7898    fn parse_corpus_store_mapping(&mut self, n: usize) -> Result<(String, Vec<String>), ParseError> {
7899        let store = self.consume(TokenType::Identifier)?.value.clone();
7900        self.consume(TokenType::LParen)?;
7901        let mut cols = Vec::with_capacity(n);
7902        for i in 0..n {
7903            if i > 0 {
7904                self.consume(TokenType::Comma)?;
7905            }
7906            cols.push(self.consume_any_ident_or_kw()?.value.clone());
7907        }
7908        self.consume(TokenType::RParen)?;
7909        Ok((store, cols))
7910    }
7911
7912    /// v2.13.0 — parse `relations: [ etype(from, to, weight) … ]`, the typed
7913    /// weighted edges of an MDN corpus graph. Entries are whitespace/newline
7914    /// separated; commas between them are optional. Edge-type validity (closed
7915    /// catalog), document references, and the weight range are checked by the
7916    /// type-checker (`check_corpus`), not here.
7917    fn parse_corpus_relations(&mut self) -> Result<Vec<CorpusRelation>, ParseError> {
7918        let mut out = Vec::new();
7919        self.consume(TokenType::LBracket)?;
7920        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
7921            if self.check(TokenType::Comma) {
7922                self.advance();
7923                continue;
7924            }
7925            let tok = self.current().clone();
7926            let etype = self.consume_any_ident_or_kw()?.value.clone();
7927            self.consume(TokenType::LParen)?;
7928            let from = self.consume_any_ident_or_kw()?.value.clone();
7929            self.consume(TokenType::Comma)?;
7930            let to = self.consume_any_ident_or_kw()?.value.clone();
7931            self.consume(TokenType::Comma)?;
7932            let weight = self.consume_number()?;
7933            self.consume(TokenType::RParen)?;
7934            out.push(CorpusRelation {
7935                etype,
7936                from,
7937                to,
7938                weight,
7939                loc: Loc { line: tok.line, column: tok.column },
7940            });
7941        }
7942        self.consume(TokenType::RBracket)?;
7943        Ok(out)
7944    }
7945
7946    /// v2.63.0 — the typed dataspace declaration:
7947    ///
7948    /// ```text
7949    /// dataspace <Name> {
7950    ///     column <name>: <Type>
7951    ///     …
7952    /// }
7953    /// ```
7954    ///
7955    /// Until 108.b the body was consumed by `skip_braced_block()` — any
7956    /// content, including garbage, compiled clean and reached nothing.
7957    /// Now each entry must be a `column` field; the declared type is
7958    /// kept RAW here and resolved against the closed 6-type catalog by
7959    /// the type-checker (`axon-T928`), so all schema errors accumulate
7960    /// in a single compile. An unknown body keyword is a parse error
7961    /// (the grammar is closed — the v1.31.0 axonstore posture).
7962    fn parse_dataspace(&mut self) -> Result<DataspaceDefinition, ParseError> {
7963        let tok = self.consume(TokenType::Dataspace)?;
7964        let name = self.consume(TokenType::Identifier)?.value;
7965        let mut node = DataspaceDefinition {
7966            name,
7967            columns: Vec::new(),
7968            loc: Loc {
7969                line: tok.line,
7970                column: tok.column,
7971            },
7972            leading_trivia: Vec::new(),
7973            trailing_trivia: Vec::new(),
7974        };
7975        if self.check(TokenType::LBrace) {
7976            self.consume(TokenType::LBrace)?;
7977            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7978                let entry = self.current().clone();
7979                if entry.value != "column" {
7980                    return Err(ParseError {
7981                        message: format!(
7982                            "Unknown entry `{}` in dataspace `{}`. A dataspace body \
7983                             declares its columnar schema: `column <name>: <Type>` \
7984                             (one per line, over the closed type catalog — \
7985                             Text, Int, Float, Bool, Timestamp, Json).",
7986                            entry.value, node.name
7987                        ),
7988                        line: entry.line,
7989                        column: entry.column,
7990                        ..Default::default()
7991                    });
7992                }
7993                self.advance(); // `column`
7994                let col_tok = self.current().clone();
7995                let col_name = self.consume_any_ident_or_kw()?.value.clone();
7996                self.consume(TokenType::Colon)?;
7997                let declared_type = self.consume_any_ident_or_kw()?.value.clone();
7998                node.columns.push(crate::ast::DataspaceColumn {
7999                    name: col_name,
8000                    declared_type,
8001                    loc: Loc {
8002                        line: col_tok.line,
8003                        column: col_tok.column,
8004                    },
8005                });
8006            }
8007            self.consume(TokenType::RBrace)?;
8008        }
8009        Ok(node)
8010    }
8011
8012    fn parse_ots(&mut self) -> Result<OtsDefinition, ParseError> {
8013        let tok = self.consume(TokenType::Ots)?;
8014        let name = self.consume(TokenType::Identifier)?.value;
8015        let mut node = OtsDefinition {
8016            name,
8017            teleology: String::new(),
8018            homotopy_search: String::new(),
8019            loss_function: String::new(),
8020            loc: Loc {
8021                line: tok.line,
8022                column: tok.column,
8023            },
8024            leading_trivia: Vec::new(),
8025            trailing_trivia: Vec::new(),
8026        };
8027        // Skip optional type params <In, Out>
8028        if self.check(TokenType::Lt) {
8029            while !self.check(TokenType::Gt) && !self.check(TokenType::Eof) {
8030                self.advance();
8031            }
8032            if self.check(TokenType::Gt) {
8033                self.advance();
8034            }
8035        }
8036        self.consume(TokenType::LBrace)?;
8037        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8038            let field_name = self.current().value.clone();
8039            self.advance();
8040            if self.check(TokenType::Colon) {
8041                self.advance();
8042                match field_name.as_str() {
8043                    "teleology" => {
8044                        node.teleology = self.consume(TokenType::StringLit)?.value.clone()
8045                    }
8046                    "homotopy_search" => {
8047                        node.homotopy_search = self.consume_any_ident_or_kw()?.value.clone()
8048                    }
8049                    // v2.83.0 — README's ots blocks write the loss as a bare
8050                    // identifier (`loss_function: SemanticPreservation`, `L2`,
8051                    // `Contrastive`); the parser accepted only a string literal, so
8052                    // all three published blocks failed at this exact token. Both
8053                    // spellings resolve to the same field.
8054                    "loss_function" => {
8055                        node.loss_function = if self.check(TokenType::StringLit) {
8056                            self.consume(TokenType::StringLit)?.value.clone()
8057                        } else {
8058                            self.consume_any_ident_or_kw()?.value.clone()
8059                        }
8060                    }
8061                    _ => self.skip_value(),
8062                }
8063            } else if self.check(TokenType::LBrace) {
8064                self.skip_braced_block()?;
8065            }
8066        }
8067        self.consume(TokenType::RBrace)?;
8068        Ok(node)
8069    }
8070
8071    fn parse_mandate(&mut self) -> Result<MandateDefinition, ParseError> {
8072        let tok = self.consume(TokenType::Mandate)?;
8073        let name = self.consume(TokenType::Identifier)?.value;
8074        let mut node = MandateDefinition {
8075            name,
8076            constraint: String::new(),
8077            kp: None,
8078            ki: None,
8079            kd: None,
8080            tolerance: None,
8081            max_steps: None,
8082            drift_bound: None,
8083            lipschitz: None,
8084            on_violation: String::new(),
8085            loc: Loc {
8086                line: tok.line,
8087                column: tok.column,
8088            },
8089            leading_trivia: Vec::new(),
8090            trailing_trivia: Vec::new(),
8091        };
8092        self.consume(TokenType::LBrace)?;
8093        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8094            let field_name = self.current().value.clone();
8095            self.advance();
8096            if self.check(TokenType::Colon) {
8097                self.advance();
8098                match field_name.as_str() {
8099                    "constraint" => {
8100                        node.constraint = self.consume(TokenType::StringLit)?.value.clone()
8101                    }
8102                    "kp" | "Kp" => node.kp = self.parse_optional_float(),
8103                    "ki" | "Ki" => node.ki = self.parse_optional_float(),
8104                    "kd" | "Kd" => node.kd = self.parse_optional_float(),
8105                    "max_steps" => node.max_steps = self.parse_optional_int(),
8106                    // v2.83.0 — `epsilon:` is what the README publishes; `tolerance:`
8107                    // is what the parser has always accepted. They are the SAME ε — the
8108                    // convergence band of `Converge(e, ε, N)`. Both spellings resolve here
8109                    // rather than one of them silently vanishing into `skip_value()`.
8110                    "tolerance" | "epsilon" => node.tolerance = self.parse_optional_float(),
8111                    "on_violation" => {
8112                        node.on_violation = self.consume_any_ident_or_kw()?.value.clone()
8113                    }
8114                    _ => self.skip_value(),
8115                }
8116            } else if self.check(TokenType::LBrace) {
8117                // v2.83.0 — `pid { Kp: 2.0, Ki: 0.3, Kd: 0.1 }`, which is the form
8118                // README XV publishes and the form every mandate example uses.
8119                //
8120                // THIS BLOCK USED TO BE `skip_braced_block()`. The consequence was not a
8121                // parse error — it was SILENT ACCEPTANCE: `axon check` printed
8122                // "0 errors" and the IR came out with `kp: None, ki: None, kd: None`.
8123                // The developer wrote the published example, the compiler agreed, and the
8124                // ENTIRE CONTROL LAW was discarded between them. A dropped specification
8125                // that reports success is the v2.67.0 defect living in the parser.
8126                if field_name == "pid" {
8127                    self.parse_pid_block(&mut node)?;
8128                } else if field_name == "stability" {
8129                    self.parse_stability_block(&mut node)?;
8130                } else {
8131                    self.skip_braced_block()?;
8132                }
8133            }
8134        }
8135        self.consume(TokenType::RBrace)?;
8136        Ok(node)
8137    }
8138
8139    /// v2.83.0 — `pid { Kp: <f>, Ki: <f>, Kd: <f> }`.
8140    ///
8141    /// The gains of the Cybernetic Refinement Calculus controller
8142    /// (`papers/paper_mandate.md` section 3): `u(t) = Kp·e(t) + Ki·∫e + Kd·de/dt`.
8143    /// Accepts both capitalised (`Kp`, the papers' and README's notation) and
8144    /// lower-case spellings, because the flat `kp:` form was already accepted and
8145    /// removing it would break programs that use it.
8146    ///
8147    /// v2.83.0 — unknown keys inside the block are REFUSED.
8148    ///
8149    /// v2.83.0 left them skipped, reasoning that the enclosing declaration behaves
8150    /// that way and tightening it was a wider decision. Measuring the published
8151    /// 2.84.0 binary showed what that costs, and the cost is not symmetric:
8152    /// misspelling a GAIN is caught (the missing gain fails the sign conditions),
8153    /// but misspelling a BOUND is not — `stability { drift: 0.5, L: 0.25 }`
8154    /// compiles clean, and the mandate is admitted with no Lyapunov floor at all.
8155    /// The typo does not weaken the check, it DELETES it.
8156    ///
8157    /// These two blocks are not like the enclosing declaration. They are closed
8158    /// catalogues of three and two keys, every one of which is a proof obligation,
8159    /// and an unrecognised key here is never a field a later version will use —
8160    /// it is a typo whose price is a silently discharged safety property.
8161    fn parse_pid_block(&mut self, node: &mut MandateDefinition) -> Result<(), ParseError> {
8162        self.consume(TokenType::LBrace)?;
8163        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8164            let key_token = self.current().clone();
8165            let key = key_token.value.clone();
8166            self.advance();
8167            if self.check(TokenType::Colon) {
8168                self.advance();
8169                match key.as_str() {
8170                    "kp" | "Kp" => node.kp = self.parse_optional_float(),
8171                    "ki" | "Ki" => node.ki = self.parse_optional_float(),
8172                    "kd" | "Kd" => node.kd = self.parse_optional_float(),
8173                    _ => {
8174                        return Err(ParseError {
8175                            message: format!(
8176                                "`{key}` is not a gain of the PID controller. The block accepts \
8177                                 exactly `Kp`, `Ki` and `Kd` (lower-case spellings too). \
8178                                 Skipping what it does not recognise would let a typo drop a \
8179                                 gain, and the stability band is computed from all three."
8180                            ),
8181                            line: key_token.line,
8182                            column: key_token.column,
8183                            ..Default::default()
8184                        });
8185                    }
8186                }
8187            }
8188            if self.check(TokenType::Comma) {
8189                self.advance();
8190            }
8191        }
8192        self.consume(TokenType::RBrace)?;
8193        Ok(())
8194    }
8195
8196    /// v2.83.0 — `stability { D: <f>, L: <f> }`.
8197    ///
8198    /// The declared hypotheses of the mandate's stability theorem: `D` is the
8199    /// drift bound `sup|drift(t)|` (paper_mandate section 3), `L` the Lipschitz
8200    /// constant of the refinement map (prompt_opt section 6.3). With them declared,
8201    /// the type checker verifies the full band `D < |Kp+Ki+Kd| < 1/L`; without
8202    /// them it can verify only the sign conditions, which the papers show to be
8203    /// necessary but not sufficient. The declaration travels in the IR as a
8204    /// proof obligation for dispatch — the compiler never invents these
8205    /// numbers, because they are measured properties of a backend it cannot
8206    /// see, and fabricating them would make the static check vacuous.
8207    ///
8208    /// An empty block is a PARSE error, not a silent no-op: `stability { }`
8209    /// asserts nothing, can discharge nothing, and the developer who wrote it
8210    /// believed otherwise.
8211    fn parse_stability_block(
8212        &mut self,
8213        node: &mut MandateDefinition,
8214    ) -> Result<(), ParseError> {
8215        let open = self.consume(TokenType::LBrace)?;
8216        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8217            let key_token = self.current().clone();
8218            let key = key_token.value.clone();
8219            self.advance();
8220            if self.check(TokenType::Colon) {
8221                self.advance();
8222                match key.as_str() {
8223                    "D" | "d" | "drift_bound" => {
8224                        node.drift_bound = self.parse_optional_float()
8225                    }
8226                    "L" | "l" | "lipschitz" => node.lipschitz = self.parse_optional_float(),
8227                    // v2.83.0 — see `parse_pid_block`. This is the arm that
8228                    // was actually dangerous: a dropped bound is a dropped
8229                    // hypothesis, and the theorem it guards then holds vacuously.
8230                    _ => {
8231                        return Err(ParseError {
8232                            message: format!(
8233                                "`{key}` is not a hypothesis of the stability theorem. The block \
8234                                 accepts exactly `D` (the drift bound, also spelled `d` or \
8235                                 `drift_bound`) and `L` (the Lipschitz constant, also `l` or \
8236                                 `lipschitz`). This is an error rather than a skipped key \
8237                                 because a bound that fails to parse is a bound that is not \
8238                                 declared, and the compiler would then verify the band it can \
8239                                 see — the sign conditions — and admit the mandate as if the \
8240                                 rest had been checked."
8241                            ),
8242                            line: key_token.line,
8243                            column: key_token.column,
8244                            ..Default::default()
8245                        });
8246                    }
8247                }
8248            }
8249            if self.check(TokenType::Comma) {
8250                self.advance();
8251            }
8252        }
8253        self.consume(TokenType::RBrace)?;
8254        if node.drift_bound.is_none() && node.lipschitz.is_none() {
8255            return Err(ParseError {
8256                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."
8257                    .to_string(),
8258                line: open.line,
8259                column: open.column,
8260                ..Default::default()
8261            });
8262        }
8263        Ok(())
8264    }
8265
8266    /// v2.67.0 — `compute <Name>(p: T, …) -> T { <expr> }`.
8267    ///
8268    /// # What this used to be
8269    ///
8270    /// ```text
8271    /// // Skip optional parameters/return type before brace
8272    /// while !self.check(TokenType::LBrace) { self.advance(); }
8273    /// ```
8274    ///
8275    /// The parameters and the return type were **skipped token by token**, and
8276    /// the brace held only `shield:`. So a `compute` had **no inputs, no output
8277    /// type and no body** — which is why the runtime could do nothing but bind
8278    /// the literal string `"compute:Name(args)"`, and why a downstream step then
8279    /// consumed that text where it expected a number. The README meanwhile
8280    /// promised "native Fast-Path execution bypassing the LLM" **with an O(n)
8281    /// guarantee**.
8282    ///
8283    /// # What it is now
8284    ///
8285    /// A named pure function over the v2.26.0 expression language — the closed,
8286    /// total, side-effect-free term algebra the runtime already evaluates
8287    /// natively (`eval_expr`, the same evaluator behind `let`, `grad` and
8288    /// `conditional`). Linear in the term, no model in the loop: the advertised
8289    /// claim, made true rather than louder.
8290    ///
8291    /// The legacy field form (`compute N { shield: G }`) still parses — its body
8292    /// is simply `None`, and applying a bodyless compute is refused (axon-T941)
8293    /// instead of silently binding a placeholder.
8294    fn parse_compute(&mut self) -> Result<ComputeDefinition, ParseError> {
8295        let tok = self.consume(TokenType::Compute)?;
8296        let name = self.consume(TokenType::Identifier)?.value;
8297        let mut node = ComputeDefinition {
8298            name,
8299            shield_ref: String::new(),
8300            parameters: Vec::new(),
8301            return_type: String::new(),
8302            body: None,
8303            loc: Loc {
8304                line: tok.line,
8305                column: tok.column,
8306            },
8307            leading_trivia: Vec::new(),
8308            trailing_trivia: Vec::new(),
8309        };
8310
8311        // `(p: T, q: T)` — the typed parameters (they used to be skipped).
8312        if self.check(TokenType::LParen) {
8313            self.advance();
8314            while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
8315                let ptok = self.current().clone();
8316                let pname = self.consume_any_ident_or_kw()?.value.clone();
8317                self.consume(TokenType::Colon)?;
8318                let ptype = self.parse_type_expr()?;
8319                node.parameters.push(Parameter {
8320                    name: pname,
8321                    type_expr: ptype,
8322                    loc: self.loc_of(&ptok),
8323                });
8324                if self.check(TokenType::Comma) {
8325                    self.advance();
8326                }
8327            }
8328            self.consume(TokenType::RParen)?;
8329        }
8330
8331        // `-> T` — the declared result type.
8332        if self.check(TokenType::Arrow) {
8333            self.advance();
8334            node.return_type = self.consume_any_ident_or_kw()?.value.clone();
8335        }
8336
8337        self.consume(TokenType::LBrace)?;
8338        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8339            // A `<name>:` pair is a legacy field (only `shield:` is meaningful).
8340            // Anything else is THE BODY — a v2.26.0 expression.
8341            //
8342            // NOTE: the field name may be a KEYWORD, not just an identifier —
8343            // `shield` is `TokenType::Shield`. Testing only for `Identifier` here
8344            // sent `compute N { shield: G }` (the legacy declaration form, and
8345            // the shape of the shipped canonical program) down the
8346            // expression-parsing path and broke it. Back-compat is not optional:
8347            // an adopter's existing program must keep compiling.
8348            let is_field = self
8349                .tokens
8350                .get(self.pos + 1)
8351                .map(|t| t.ttype == TokenType::Colon)
8352                .unwrap_or(false);
8353            if is_field {
8354                let field_tok = self.current().clone();
8355                let field_name = self.current().value.clone();
8356                self.advance();
8357                self.consume(TokenType::Colon)?;
8358                match field_name.as_str() {
8359                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
8360                    // v2.83.0 — `input: a (Float), b (Float)`.
8361                    //
8362                    // This is the parameter list EVERY published compute writes,
8363                    // and it was reaching `skip_value()` — silently discarded, so
8364                    // a compute declared this way had no parameters at all and
8365                    // `run_compute_apply` refused it on arity. The typed form
8366                    // `(a: Float, b: Float)` above stays accepted; both fill the
8367                    // same `parameters`, because they are one concept spelled two
8368                    // ways and a second slot would let them disagree.
8369                    "input" => self.parse_compute_input_list(&mut node)?,
8370                    // v2.83.0 — `output: Float` / `output: PremiumResult`,
8371                    // the field spelling of `-> T`.
8372                    "output" => {
8373                        node.return_type = self.parse_output_type_string()?;
8374                    }
8375                    _ => self.skip_value(),
8376                }
8377                let _ = field_tok;
8378            } else if self.current().value == "logic"
8379                && self
8380                    .tokens
8381                    .get(self.pos + 1)
8382                    .is_some_and(|t| t.ttype == TokenType::LBrace)
8383            {
8384                // v2.83.0 — `logic { let … return … }`, the body form all
8385                // four published computes write. It used to fall to
8386                // `parse_expr()`, which met the bare word `logic` and produced a
8387                // diagnostic about an expression the author never wrote.
8388                if node.body.is_some() {
8389                    return Err(ParseError {
8390                        message: "compute declares two bodies; a pure function has one result, \
8391                                  and keeping the last silently would discard the first"
8392                            .to_string(),
8393                        line: self.current().line,
8394                        column: self.current().column,
8395                        ..Default::default()
8396                    });
8397                }
8398                node.body = Some(self.parse_logic_block()?);
8399            } else {
8400                node.body = Some(self.parse_expr()?);
8401            }
8402        }
8403        self.consume(TokenType::RBrace)?;
8404        Ok(node)
8405    }
8406
8407    /// v2.83.0 — `input: base_rate (Float), risk_factor (Float)`.
8408    ///
8409    /// The published spelling inverts the typed form's punctuation: the name
8410    /// comes first and the type rides in parentheses. Both land in
8411    /// `ComputeDefinition::parameters`.
8412    fn parse_compute_input_list(&mut self, node: &mut ComputeDefinition) -> Result<(), ParseError> {
8413        loop {
8414            let ptok = self.current().clone();
8415            let pname = self.consume_any_ident_or_kw()?.value.clone();
8416            // The type is optional in principle; every published compute writes
8417            // it, and a parameter with no declared type cannot be checked, so an
8418            // absent one is recorded as empty rather than invented.
8419            let type_expr = if self.check(TokenType::LParen) {
8420                self.advance();
8421                let t = self.parse_type_expr()?;
8422                self.consume(TokenType::RParen)?;
8423                t
8424            } else {
8425                TypeExpr {
8426                    name: String::new(),
8427                    generic_param: String::new(),
8428                    optional: false,
8429                    loc: self.loc_of(&ptok),
8430                }
8431            };
8432            node.parameters.push(Parameter {
8433                name: pname,
8434                type_expr,
8435                loc: self.loc_of(&ptok),
8436            });
8437            if self.check(TokenType::Comma) {
8438                self.advance();
8439            } else {
8440                break;
8441            }
8442        }
8443        Ok(())
8444    }
8445
8446    /// v2.83.0 — the `logic { }` body: a chain of `let`s closed by `return`.
8447    ///
8448    /// Lowered to nested [`Expr::Let`] terms, innermost-last, so
8449    /// `let a = e₁  let b = e₂  return e₃` becomes `Let(a, e₁, Let(b, e₂, e₃))`.
8450    /// That is one evaluation per binding — substituting the bindings into the
8451    /// return expression instead would re-evaluate every bound term once per
8452    /// mention.
8453    ///
8454    /// `return` is REQUIRED. A `logic` block whose last statement is a `let`
8455    /// binds names and produces nothing; the compute would then have to invent a
8456    /// result, and inventing the result of a deterministic function is the one
8457    /// thing this primitive exists not to do.
8458    fn parse_logic_block(&mut self) -> Result<Expr, ParseError> {
8459        let open = self.current().clone();
8460        self.advance(); // `logic`
8461        self.consume(TokenType::LBrace)?;
8462
8463        let mut bindings: Vec<(String, Expr)> = Vec::new();
8464        let mut result: Option<Expr> = None;
8465
8466        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8467            if self.check(TokenType::Let) {
8468                if result.is_some() {
8469                    return Err(ParseError {
8470                        message: "a `let` after the `return` in a `logic { }` block is \
8471                                  unreachable — the block's value is already decided. Move it \
8472                                  above the `return`."
8473                            .to_string(),
8474                        line: self.current().line,
8475                        column: self.current().column,
8476                        ..Default::default()
8477                    });
8478                }
8479                self.advance(); // `let`
8480                let name = self.consume_any_ident_or_kw()?.value.clone();
8481                self.consume(TokenType::Assign)?;
8482                bindings.push((name, self.parse_expr()?));
8483            } else if self.check(TokenType::Return) {
8484                self.advance();
8485                result = Some(self.parse_expr()?);
8486            } else {
8487                let bad = self.current().clone();
8488                return Err(ParseError {
8489                    message: format!(
8490                        "unexpected `{}` in a `logic {{ }}` block — it admits only `let <name> = \
8491                         <expr>` bindings and a closing `return <expr>`. `compute` is a PURE \
8492                         function (its own paper: \"pureza categórica de los morfismos \
8493                         funcionales\"), so a statement that could have an effect is refused \
8494                         rather than parsed and dropped.",
8495                        bad.value
8496                    ),
8497                    line: bad.line,
8498                    column: bad.column,
8499                    ..Default::default()
8500                });
8501            }
8502        }
8503        self.consume(TokenType::RBrace)?;
8504
8505        let mut expr = result.ok_or_else(|| ParseError {
8506            message: "a `logic { }` block must end in `return <expr>`. Without it the block binds \
8507                      names and yields nothing, and the compute would have to invent a result — \
8508                      which is precisely what a deterministic primitive must never do."
8509                .to_string(),
8510            line: open.line,
8511            column: open.column,
8512            ..Default::default()
8513        })?;
8514
8515        // Fold innermost-last so the first `let` written is the outermost scope.
8516        for (name, value) in bindings.into_iter().rev() {
8517            expr = Expr::Let {
8518                name,
8519                value: Box::new(value),
8520                body: Box::new(expr),
8521            };
8522        }
8523        Ok(expr)
8524    }
8525
8526    fn parse_daemon(&mut self) -> Result<DaemonDefinition, ParseError> {
8527        let tok = self.consume(TokenType::Daemon)?;
8528        let name = self.consume(TokenType::Identifier)?.value;
8529        let mut node = DaemonDefinition {
8530            name,
8531            goal: String::new(),
8532            tools: Vec::new(),
8533            memory_ref: String::new(),
8534            strategy: String::new(),
8535            on_stuck: String::new(),
8536            shield_ref: String::new(),
8537            window_ref: String::new(),
8538            budget: None,
8539            max_tokens: None,
8540            max_time: String::new(),
8541            max_cost: None,
8542            listeners: Vec::new(),
8543            requires_capabilities: Vec::new(),
8544            loc: Loc {
8545                line: tok.line,
8546                column: tok.column,
8547            },
8548            leading_trivia: Vec::new(),
8549            trailing_trivia: Vec::new(),
8550        };
8551        // Skip optional parameters/return type before brace
8552        while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
8553            self.advance();
8554        }
8555        self.consume(TokenType::LBrace)?;
8556        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8557            let field = self.current().clone();
8558            let field_name = field.value.clone();
8559            self.advance();
8560            if self.check(TokenType::Colon) {
8561                self.advance();
8562                match field_name.as_str() {
8563                    "goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
8564                    "tools" => node.tools = self.parse_bracketed_identifiers()?,
8565                    "memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
8566                    "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
8567                    "on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
8568                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
8569                    // v2.27.0 — `window: <WindowName>` temporal binding.
8570                    "window" => node.window_ref = self.consume_any_ident_or_kw()?.value.clone(),
8571                    "max_tokens" => node.max_tokens = self.parse_optional_int(),
8572                    "max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
8573                    "max_cost" => node.max_cost = self.parse_optional_float(),
8574                    // v2.4.0 — `requires: [cap, …]` capability scope (same
8575                    // closed slug grammar as `axonendpoint requires:`). The
8576                    // enterprise supervisor mints a per-run principal scoped to
8577                    // exactly these (least privilege).
8578                    "requires" => {
8579                        let bracket_tok = self.current().clone();
8580                        let items = self.parse_bracketed_dot_identifiers()?;
8581                        for slug in &items {
8582                            if !is_valid_capability_slug(slug) {
8583                                return Err(ParseError {
8584                                    message: format!(
8585                                        "Invalid capability slug '{slug}' in daemon '{}' \
8586                                         `requires:`. Capability slugs must match \
8587                                         ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
8588                                         lowercase identifiers. Examples: `daemon.run`, \
8589                                         `memory.write`, `flow.execute`.",
8590                                        node.name
8591                                    ),
8592                                    line: bracket_tok.line,
8593                                    column: bracket_tok.column,
8594                                    ..Default::default()
8595                                });
8596                            }
8597                        }
8598                        node.requires_capabilities = items;
8599                    }
8600                    _ => self.skip_value(),
8601                }
8602            } else if field.ttype == TokenType::Listen {
8603                // v1.6.0 D4 — preserve listen blocks for type
8604                // checking.  We backtracked past the `listen` keyword
8605                // by `advance()` above, so reconstruct a synthetic
8606                // listener using the same dual-mode dispatch the flow
8607                // step parser uses (string topic OR typed channel ref).
8608                let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
8609                    (self.consume(TokenType::StringLit)?.value.clone(), false)
8610                } else {
8611                    (self.consume_any_ident_or_kw()?.value.clone(), true)
8612                };
8613                let mut alias = String::new();
8614                if !self.at_declaration_start()
8615                    && !self.check(TokenType::RBrace)
8616                    && !self.check(TokenType::LBrace)
8617                {
8618                    let next = self.current().clone();
8619                    if next.value == "as" || next.ttype == TokenType::As {
8620                        self.advance();
8621                        alias = self.consume_any_ident_or_kw()?.value.clone();
8622                    }
8623                }
8624                let listen_loc = Loc {
8625                    line: field.line,
8626                    column: field.column,
8627                };
8628                // v2.4.0 — parse the handler body (was skipped). This is
8629                // what makes a `daemon` operational: the body runs per event /
8630                // scheduled tick (e.g. a `listen "cron:…" as tick { run … }`).
8631                let body = self.parse_listener_body()?;
8632                node.listeners.push(ListenStep {
8633                    channel,
8634                    channel_is_ref,
8635                    event_alias: alias,
8636                    body,
8637                    loc: listen_loc,
8638                });
8639            } else if field_name == "budget" && self.check(TokenType::LBrace) {
8640                // v2.28.0 — the `budget { … }` linear-effect rate-limit block.
8641                node.budget = Some(self.parse_budget_block(field.line, field.column)?);
8642            } else if self.check(TokenType::LBrace) {
8643                self.skip_braced_block()?;
8644            }
8645        }
8646        self.consume(TokenType::RBrace)?;
8647        Ok(node)
8648    }
8649
8650    /// v2.69.0 — a TOP-LEVEL `budget <Name> { … }`.
8651    ///
8652    /// Same body as the daemon-attached block; what it gains is a **name** and a
8653    /// **scope that is not a daemon**. Until v2.69.0, `budget` was a field of `daemon`
8654    /// and of nothing else — so an adopter deploying an HTTP endpoint that calls a
8655    /// vendor tool had **no way in the language to bound how often it did that.**
8656    /// Not "the bound did not work": **the bound could not be written.** And the
8657    /// HTTP endpoint is what people actually deploy.
8658    fn parse_top_level_budget(&mut self) -> Result<BudgetBlock, ParseError> {
8659        let kw = self.consume(TokenType::Budget)?; // `budget`
8660        let name = self.consume(TokenType::Identifier)?.value;
8661        let mut block = self.parse_budget_block(kw.line, kw.column)?;
8662        block.name = name;
8663        Ok(block)
8664    }
8665
8666    /// v2.28.0 — `budget { <rate|max>: N per <period> on Tool(<X>) … [on_exhausted: <p>] }`.
8667    fn parse_budget_block(&mut self, line: u32, column: u32) -> Result<BudgetBlock, ParseError> {
8668        self.consume(TokenType::LBrace)?;
8669        let mut quotas = Vec::new();
8670        let mut on_exhausted = String::new();
8671        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8672            let field = self.current().clone();
8673            let field_name = self.consume_any_ident_or_kw()?.value;
8674            match field_name.as_str() {
8675                "rate" | "max" => {
8676                    quotas.push(self.parse_budget_quota(field_name, field.line, field.column)?);
8677                }
8678                "on_exhausted" => {
8679                    self.consume(TokenType::Colon)?;
8680                    on_exhausted = self.consume_any_ident_or_kw()?.value;
8681                }
8682                _ => self.skip_value(),
8683            }
8684        }
8685        self.consume(TokenType::RBrace)?;
8686        Ok(BudgetBlock {
8687            name: String::new(),
8688            quotas,
8689            on_exhausted,
8690            loc: Loc { line, column },
8691            leading_trivia: Vec::new(),
8692            trailing_trivia: Vec::new(),
8693        })
8694    }
8695
8696    /// v2.28.0 — one quota line: `<kind>: <limit> per <period> on Tool(<effect>)`.
8697    /// `kind` (`rate`/`max`) is already consumed by the caller.
8698    fn parse_budget_quota(
8699        &mut self,
8700        kind: String,
8701        line: u32,
8702        column: u32,
8703    ) -> Result<BudgetQuota, ParseError> {
8704        self.consume(TokenType::Colon)?;
8705        let limit = self.consume_number()? as i64;
8706        // `per <period>`
8707        let _per = self.consume_any_ident_or_kw()?; // the `per` keyword
8708        let period = self.consume_any_ident_or_kw()?.value;
8709        // `on Tool(<effect>)`
8710        let _on = self.consume_any_ident_or_kw()?; // the `on` keyword
8711        let _tool = self.consume_any_ident_or_kw()?; // the `Tool` wrapper keyword
8712        self.consume(TokenType::LParen)?;
8713        let effect = self.consume_any_ident_or_kw()?.value;
8714        self.consume(TokenType::RParen)?;
8715        Ok(BudgetQuota {
8716            kind,
8717            limit,
8718            period,
8719            effect,
8720            loc: Loc { line, column },
8721        })
8722    }
8723
8724    fn parse_axonstore(&mut self) -> Result<AxonStoreDefinition, ParseError> {
8725        let tok = self.consume(TokenType::AxonStore)?;
8726        let name = self.consume(TokenType::Identifier)?.value;
8727        let mut node = AxonStoreDefinition {
8728            name,
8729            backend: String::new(),
8730            connection: String::new(),
8731            resource_ref: String::new(),
8732            confidence_floor: None,
8733            isolation: String::new(),
8734            on_breach: String::new(),
8735            capability: String::new(),
8736            class: String::new(),
8737            column_schema: None,
8738            loc: Loc {
8739                line: tok.line,
8740                column: tok.column,
8741            },
8742            leading_trivia: Vec::new(),
8743            trailing_trivia: Vec::new(),
8744        };
8745        self.consume(TokenType::LBrace)?;
8746        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8747            let field = self.current().clone();
8748            let field_name = field.value.clone();
8749            // v1.31.0 (D1) — `schema:` declaration in three closed
8750            // forms: inline column block, manifest reference (string
8751            // literal), or env-var schema namespace (`env:VAR` —
8752            // unquoted or quoted). Parse the form; the v1.31.0 / v1.31.0
8753            // type-checker consumes the resulting AST.
8754            if field.ttype == TokenType::Schema {
8755                self.advance();
8756                let parsed = self.parse_store_schema_declaration(&node.name, field.line, field.column)?;
8757                node.column_schema = Some(parsed);
8758                continue;
8759            }
8760            self.advance();
8761            if self.check(TokenType::Colon) {
8762                self.advance();
8763                match field_name.as_str() {
8764                    "backend" => node.backend = self.consume_any_ident_or_kw()?.value.clone(),
8765                    // v2.48.0 — the secret-class prefix of a
8766                    // `backend: secrets` metadata store. Dotted-identifier
8767                    // form (`class: crm`, `class: crm.oauth`); the
8768                    // secrets-only placement rule + slug shape are
8769                    // `axon-T900` in the type-checker (it needs the
8770                    // resolved `backend:`, which may appear after this
8771                    // field in source order).
8772                    "class" => node.class = self.parse_dotted_identifier()?,
8773                    "connection" => node.connection = self.parse_config_key()?,
8774                    // v2.67.0 — the `resource` this store RUNS ON. When
8775                    // present the store derives its DSN, its POOL SIZE and its
8776                    // sharing discipline from the resource; `connection:`
8777                    // becomes redundant and `axon-T946` refuses declaring both
8778                    // (the same fact, twice, is how the islands happened).
8779                    "resource" => {
8780                        node.resource_ref = self.consume_any_ident_or_kw()?.value.clone()
8781                    }
8782                    "confidence_floor" => node.confidence_floor = self.parse_optional_float(),
8783                    "isolation" => node.isolation = self.consume_any_ident_or_kw()?.value.clone(),
8784                    "on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
8785                    // v1.30.0 (D11) — Pillar IV: the capability slug
8786                    // required to access this store. Validated against
8787                    // the closed slug grammar shared with `requires:`.
8788                    "capability" => {
8789                        let slug_tok = self.consume(TokenType::StringLit)?.clone();
8790                        if !is_valid_capability_slug(&slug_tok.value) {
8791                            return Err(ParseError {
8792                                message: format!(
8793                                    "Invalid capability slug '{}' in axonstore '{}' \
8794                                     `capability:`. Capability slugs must match \
8795                                     ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
8796                                     lowercase identifiers starting with a letter. Examples: \
8797                                     `admin`, `tenant.read`, `hipaa.phi.read`.",
8798                                    slug_tok.value, node.name
8799                                ),
8800                                line: slug_tok.line,
8801                                column: slug_tok.column,
8802                                ..Default::default()
8803                            });
8804                        }
8805                        node.capability = slug_tok.value.clone();
8806                    }
8807                    _ => self.skip_value(),
8808                }
8809            } else if self.check(TokenType::LBrace) {
8810                self.skip_braced_block()?;
8811            }
8812        }
8813        self.consume(TokenType::RBrace)?;
8814        Ok(node)
8815    }
8816
8817    /// v1.31.0 (D1) — parse the three closed forms of an `axonstore`
8818    /// `schema:` declaration:
8819    ///
8820    ///   * form (a) **inline** — `schema { col: Type [constraint…], … }`
8821    ///   * form (b) **manifest reference** — `schema: "qualified.name"`
8822    ///     (string literal that does NOT start with `env:`)
8823    ///   * form (c) **env-var schema namespace** — `schema: env:VAR`
8824    ///     (unquoted) OR `schema: "env:VAR"` (quoted; the literal
8825    ///     starts with `env:`)
8826    ///
8827    /// Called immediately AFTER `schema` is consumed.
8828    fn parse_store_schema_declaration(
8829        &mut self,
8830        store_name: &str,
8831        sch_line: u32,
8832        sch_col: u32,
8833    ) -> Result<crate::store_schema::StoreColumnSchema, ParseError> {
8834        use crate::store_schema::{StoreColumn, StoreColumnSchema, StoreColumnType};
8835
8836        // — Form (a) — inline column block: `schema { ... }`. —
8837        if self.check(TokenType::LBrace) {
8838            self.consume(TokenType::LBrace)?;
8839            let mut columns: Vec<StoreColumn> = Vec::new();
8840            while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8841                let col_tok = self.current().clone();
8842                let col_name = self.consume_any_ident_or_kw()?.value.clone();
8843                self.consume(TokenType::Colon)?;
8844                let type_tok = self.consume_any_ident_or_kw()?.clone();
8845                let col_type = StoreColumnType::from_token(&type_tok.value).ok_or_else(|| {
8846                    let names = StoreColumnType::all_canonical_names();
8847                    let suggestion =
8848                        crate::smart_suggest::suggest_for(&type_tok.value, &names);
8849                    let suggest_suffix = if suggestion.is_empty() {
8850                        String::new()
8851                    } else {
8852                        format!(" {suggestion}")
8853                    };
8854                    let known = names.join(", ");
8855                    ParseError {
8856                        message: format!(
8857                            "Unknown column type `{}` for column `{}` in \
8858                             axonstore `{}` `schema:` block. The closed \
8859                             v1.38.0 column-type catalog \
8860                             is {{{known}}} (plus common lowercase \
8861                             aliases — `int`/`integer`/`int4` for \
8862                             `Int`, `bool`/`boolean` for `Bool`, etc.).\
8863                             {suggest_suffix}",
8864                            type_tok.value, col_name, store_name
8865                        ),
8866                        line: type_tok.line,
8867                        column: type_tok.column,
8868                        ..Default::default()
8869                    }
8870                })?;
8871
8872                // v2.26.0 (D1) — the OPTIONAL `Json<T>` shape LENS on a
8873                // column. `payload: Json<UserEvent>` records the expected
8874                // struct shape; the lens is a compile-time expectation only
8875                // (the column stays physically `jsonb`, navigated totally at
8876                // runtime — doctrine `open_data_is_total`). The shape's
8877                // well-formedness (T is a declared `type`) is `axon-T840`
8878                // in the type-checker — it needs the symbol table. Here we
8879                // only enforce the STRUCTURAL rule: a `<T>` lens may refine
8880                // ONLY a `Json` / `Jsonb` column — `axon-T841` otherwise.
8881                let mut json_shape: Option<String> = None;
8882                if self.check(TokenType::Lt) {
8883                    self.advance();
8884                    let shape_tok = self.consume_any_ident_or_kw()?.clone();
8885                    self.consume(TokenType::Gt)?;
8886                    if matches!(col_type, StoreColumnType::Json | StoreColumnType::Jsonb) {
8887                        json_shape = Some(shape_tok.value.clone());
8888                    } else {
8889                        return Err(ParseError {
8890                            message: format!(
8891                                "axon-T841 a shape lens `<{shape}>` may refine \
8892                                 only a `Json` / `Jsonb` column, but column \
8893                                 `{col}` in axonstore `{store}` is `{ty}`. Drop \
8894                                 the `<{shape}>` (a rigid column already has a \
8895                                 fixed shape), or change the column type to \
8896                                 `Json<{shape}>` if it carries open documents.",
8897                                shape = shape_tok.value,
8898                                col = col_name,
8899                                store = store_name,
8900                                ty = col_type.canonical_name(),
8901                            ),
8902                            line: shape_tok.line,
8903                            column: shape_tok.column,
8904                            ..Default::default()
8905                        });
8906                    }
8907                }
8908
8909                let mut col = StoreColumn {
8910                    name: col_name,
8911                    col_type,
8912                    json_shape,
8913                    primary_key: false,
8914                    auto_increment: false,
8915                    not_null: false,
8916                    unique: false,
8917                    indexed: false,
8918                    default_value: String::new(),
8919                    // v1.31.0 (D1) — `identity` is now a recognized
8920                    // inline keyword (see the constraint loop below).
8921                    // Defaults to false; set to true when the adopter
8922                    // writes `id: BigInt primary_key identity`.
8923                    identity: false,
8924                    line: col_tok.line,
8925                    column: col_tok.column,
8926                };
8927
8928                // Trailing constraints (position-independent), matching
8929                // the Python `_parse_store_column` surface.
8930                while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8931                    if self.current().ttype != TokenType::Identifier {
8932                        // The next column starts with a non-identifier
8933                        // (rare) — stop the constraint scan.
8934                        break;
8935                    }
8936                    let constraint = self.current().value.clone();
8937                    match constraint.as_str() {
8938                        "primary_key" => {
8939                            col.primary_key = true;
8940                            self.advance();
8941                        }
8942                        "auto_increment" => {
8943                            col.auto_increment = true;
8944                            self.advance();
8945                        }
8946                        "not_null" => {
8947                            col.not_null = true;
8948                            self.advance();
8949                        }
8950                        "unique" => {
8951                            col.unique = true;
8952                            self.advance();
8953                        }
8954                        // v2.26.0 (D1) — the `index` constraint declares
8955                        // an index as a capability-honest effect (visible to
8956                        // the deploy gate, not a silent DBA action). The
8957                        // backend picks the method from the column type
8958                        // (GIN for a Json/Jsonb column, b-tree otherwise).
8959                        "index" => {
8960                            col.indexed = true;
8961                            self.advance();
8962                        }
8963                        // v1.31.0 (D1) — `identity` marks a column
8964                        // as `GENERATED ALWAYS/BY DEFAULT AS IDENTITY`.
8965                        // Distinct from `auto_increment` (legacy SERIAL
8966                        // via `nextval(...)` default). T803 skips
8967                        // identity columns from the NOT-NULL-omission
8968                        // check because Postgres auto-fills them; the
8969                        // distinction matters because IDENTITY ALWAYS
8970                        // also rejects user-supplied values, where
8971                        // SERIAL accepts them (a future 38.x.e arm in
8972                        // T802 may surface this).
8973                        "identity" => {
8974                            col.identity = true;
8975                            self.advance();
8976                        }
8977                        "default" => {
8978                            self.advance();
8979                            let dv = self.current().clone();
8980                            if matches!(
8981                                dv.ttype,
8982                                TokenType::StringLit
8983                                    | TokenType::Integer
8984                                    | TokenType::Float
8985                            ) {
8986                                col.default_value = dv.value.clone();
8987                                self.advance();
8988                            } else {
8989                                col.default_value =
8990                                    self.consume_any_ident_or_kw()?.value.clone();
8991                            }
8992                        }
8993                        _ => break,
8994                    }
8995                }
8996
8997                columns.push(col);
8998            }
8999            self.consume(TokenType::RBrace)?;
9000            return Ok(StoreColumnSchema::Inline {
9001                columns,
9002                leading_trivia: Vec::new(),
9003                line: sch_line,
9004                column: sch_col,
9005            });
9006        }
9007
9008        // — Forms (b) + (c) require a `:` separator. —
9009        if !self.check(TokenType::Colon) {
9010            let cur = self.current().clone();
9011            return Err(ParseError {
9012                message: format!(
9013                    "axonstore `{store_name}` `schema:` declaration expects \
9014                     `{{ … }}` (inline columns), `: \"manifest.ref\"` \
9015                     (manifest reference), or `: env:VAR` (per-tenant schema \
9016                     namespace). Got `{}` instead.",
9017                    cur.value
9018                ),
9019                line: cur.line,
9020                column: cur.column,
9021                ..Default::default()
9022            });
9023        }
9024        self.consume(TokenType::Colon)?;
9025
9026        // — Form (b) or (c)-quoted — string literal value. —
9027        if self.check(TokenType::StringLit) {
9028            let lit = self.consume(TokenType::StringLit)?.clone();
9029            let value = lit.value.clone();
9030            if let Some(var) = value.strip_prefix("env:") {
9031                let var = var.trim();
9032                if var.is_empty() {
9033                    return Err(ParseError {
9034                        message: format!(
9035                            "axonstore `{store_name}` `schema: \"env:\"` is \
9036                             missing the variable name after the `env:` \
9037                             prefix."
9038                        ),
9039                        line: lit.line,
9040                        column: lit.column,
9041                        ..Default::default()
9042                    });
9043                }
9044                return Ok(StoreColumnSchema::EnvVar {
9045                    var_name: var.to_string(),
9046                    line: sch_line,
9047                    column: sch_col,
9048                });
9049            }
9050            // Plain string → manifest reference.
9051            if value.trim().is_empty() {
9052                return Err(ParseError {
9053                    message: format!(
9054                        "axonstore `{store_name}` `schema:` manifest reference \
9055                         is empty. Expected `\"qualified.name\"` — e.g. \
9056                         `\"public.tenants\"`."
9057                    ),
9058                    line: lit.line,
9059                    column: lit.column,
9060                    ..Default::default()
9061                });
9062            }
9063            return Ok(StoreColumnSchema::ManifestRef {
9064                qualified_name: value,
9065                line: sch_line,
9066                column: sch_col,
9067            });
9068        }
9069
9070        // — Form (c) unquoted — `env:VAR`. The lexer emits `env` as an
9071        //   identifier, then `:`, then the identifier var name. —
9072        let env_tok = self.current().clone();
9073        if env_tok.value == "env" {
9074            self.advance();
9075            if !self.check(TokenType::Colon) {
9076                return Err(ParseError {
9077                    message: format!(
9078                        "axonstore `{store_name}` `schema: env` is missing the \
9079                         `:` separator. Expected `schema: env:VAR`."
9080                    ),
9081                    line: env_tok.line,
9082                    column: env_tok.column,
9083                    ..Default::default()
9084                });
9085            }
9086            self.advance(); // past ':'
9087            let var_tok = self.consume_any_ident_or_kw()?.clone();
9088            if var_tok.value.trim().is_empty() {
9089                return Err(ParseError {
9090                    message: format!(
9091                        "axonstore `{store_name}` `schema: env:` is missing \
9092                         the variable name."
9093                    ),
9094                    line: var_tok.line,
9095                    column: var_tok.column,
9096                    ..Default::default()
9097                });
9098            }
9099            return Ok(StoreColumnSchema::EnvVar {
9100                var_name: var_tok.value.clone(),
9101                line: sch_line,
9102                column: sch_col,
9103            });
9104        }
9105
9106        Err(ParseError {
9107            message: format!(
9108                "axonstore `{store_name}` `schema:` declaration expects \
9109                 `{{ … }}` (inline columns), `\"manifest.ref\"` (manifest \
9110                 reference), or `env:VAR` (per-tenant schema namespace). \
9111                 Got `{}` instead.",
9112                env_tok.value
9113            ),
9114            line: env_tok.line,
9115            column: env_tok.column,
9116            ..Default::default()
9117        })
9118    }
9119
9120    // ── v1.1.0 — Resource primitive ────────────────────────
9121
9122    /// Parse: `resource Name { kind, endpoint, capacity, lifetime, certainty_floor, shield }`.
9123    ///
9124    /// Mirrors `axon.compiler.parser.Parser._parse_resource`. Unknown fields
9125    /// are silently skipped (keeps the grammar forward-compatible).
9126    fn parse_resource(&mut self) -> Result<ResourceDefinition, ParseError> {
9127        let tok = self.consume(TokenType::Resource)?;
9128        let name = self.consume(TokenType::Identifier)?.value;
9129        let mut node = ResourceDefinition {
9130            name,
9131            kind: String::new(),
9132            endpoint: String::new(),
9133            capacity: None,
9134            lifetime: "affine".to_string(),
9135            certainty_floor: None,
9136            shield_ref: String::new(),
9137            within: String::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_tok = self.current().clone();
9148            let field_name = field_tok.value.clone();
9149            self.advance();
9150            if !self.check(TokenType::Colon) {
9151                // Tolerate stray brace or unknown layout.
9152                if self.check(TokenType::LBrace) {
9153                    self.skip_braced_block()?;
9154                }
9155                continue;
9156            }
9157            self.advance(); // past ':'
9158            match field_name.as_str() {
9159                "kind" => node.kind = self.consume_any_ident_or_kw()?.value,
9160                // v2.67.0 — `endpoint:` accepts BOTH shapes on purpose:
9161                //   - a dotted config key  (`endpoint: db.main`)      — the law
9162                //   - a string literal     (`endpoint: "postgres://…"`) — the sin
9163                //
9164                // The literal is REFUSED, but by `axon-T944`, not by the parser.
9165                // If it died here the adopter would read "Expected StringLit",
9166                // which explains nothing. The law gets to say why: *URLs and
9167                // credentials never appear in source* — the same sentence
9168                // `axon-T850` has been saying to `upstream.resolve` all along.
9169                //
9170                // A diagnostic that names the rule teaches; one that names the
9171                // token type only tells you the compiler is unhappy.
9172                "endpoint" => {
9173                    node.endpoint = if self.check(TokenType::StringLit) {
9174                        self.consume(TokenType::StringLit)?.value
9175                    } else {
9176                        self.parse_dotted_identifier()?
9177                    };
9178                }
9179                "capacity" => {
9180                    node.capacity = self.parse_optional_int();
9181                }
9182                "lifetime" => {
9183                    let lt_tok = self.consume_any_ident_or_kw()?;
9184                    let lt = lt_tok.value;
9185                    if !matches!(lt.as_str(), "linear" | "affine" | "persistent") {
9186                        return Err(ParseError {
9187                            message: format!(
9188                                "Invalid lifetime '{lt}' in resource '{}' — \
9189                                 expected linear | affine | persistent",
9190                                node.name
9191                            ),
9192                            line: lt_tok.line,
9193                            column: lt_tok.column,
9194                                                    ..Default::default()
9195                        });
9196                    }
9197                    node.lifetime = lt;
9198                }
9199                "certainty_floor" => {
9200                    node.certainty_floor = self.parse_optional_float();
9201                }
9202                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
9203                // v2.67.0 — `within: <fabric>`. ONE field, so a resource
9204                // cannot be in two fabrics: Separation-Logic disjointness is
9205                // unrepresentable rather than verified.
9206                "within" => node.within = self.consume_any_ident_or_kw()?.value,
9207                // v2.67.0 — an unknown field is a HARD ERROR, not a shrug.
9208                //
9209                // This arm used to be `_ => self.skip_value()`. That is the same
9210                // family as v2.67.0's root cause (`parse_block_step` →
9211                // `skip_braced_block()`, which silently killed four primitives):
9212                // a misspelled `withn:` would have been swallowed without a
9213                // word, and the resource would have governed nothing while
9214                // looking governed. A field the parser does not know is a field
9215                // the adopter believes in and the compiler does not.
9216                unknown => {
9217                    return Err(ParseError {
9218                        message: format!(
9219                            "Unknown field '{unknown}' in resource '{}' — expected one of: \
9220                             kind, endpoint, capacity, lifetime, certainty_floor, shield, within",
9221                            node.name
9222                        ),
9223                        line: field_tok.line,
9224                        column: field_tok.column,
9225                        ..Default::default()
9226                    });
9227                }
9228            }
9229        }
9230        self.consume(TokenType::RBrace)?;
9231        Ok(node)
9232    }
9233
9234    /// Parse: `fabric Name { provider, region, zones, ephemeral, shield }`.
9235    fn parse_fabric(&mut self) -> Result<FabricDefinition, ParseError> {
9236        let tok = self.consume(TokenType::Fabric)?;
9237        let name = self.consume(TokenType::Identifier)?.value;
9238        let mut node = FabricDefinition {
9239            name,
9240            provider: String::new(),
9241            region: String::new(),
9242            zones: None,
9243            ephemeral: None,
9244            shield_ref: String::new(),
9245            loc: Loc {
9246                line: tok.line,
9247                column: tok.column,
9248            },
9249            leading_trivia: Vec::new(),
9250            trailing_trivia: Vec::new(),
9251        };
9252        self.consume(TokenType::LBrace)?;
9253        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9254            let field_name = self.current().value.clone();
9255            self.advance();
9256            if !self.check(TokenType::Colon) {
9257                if self.check(TokenType::LBrace) {
9258                    self.skip_braced_block()?;
9259                }
9260                continue;
9261            }
9262            self.advance(); // past ':'
9263            match field_name.as_str() {
9264                "provider" => node.provider = self.consume_any_ident_or_kw()?.value,
9265                "region" => node.region = self.consume(TokenType::StringLit)?.value,
9266                "zones" => node.zones = self.parse_optional_int(),
9267                "ephemeral" => {
9268                    let b = self.parse_bool()?;
9269                    node.ephemeral = Some(b);
9270                }
9271                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
9272                _ => self.skip_value(),
9273            }
9274        }
9275        self.consume(TokenType::RBrace)?;
9276        Ok(node)
9277    }
9278
9279    /// Parse: `manifest Name { resources, fabric, region, zones, compliance }`.
9280    fn parse_manifest(&mut self) -> Result<ManifestDefinition, ParseError> {
9281        let tok = self.consume(TokenType::Manifest)?;
9282        let name = self.consume(TokenType::Identifier)?.value;
9283        let mut node = ManifestDefinition {
9284            name,
9285            resources: Vec::new(),
9286            fabric_ref: String::new(),
9287            region: String::new(),
9288            zones: None,
9289            census: String::new(),
9290            compliance: Vec::new(),
9291            loc: Loc {
9292                line: tok.line,
9293                column: tok.column,
9294            },
9295            leading_trivia: Vec::new(),
9296            trailing_trivia: Vec::new(),
9297        };
9298        self.consume(TokenType::LBrace)?;
9299        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9300            let field_name = self.current().value.clone();
9301            self.advance();
9302            if !self.check(TokenType::Colon) {
9303                if self.check(TokenType::LBrace) {
9304                    self.skip_braced_block()?;
9305                }
9306                continue;
9307            }
9308            self.advance();
9309            match field_name.as_str() {
9310                "resources" => node.resources = self.parse_bracketed_identifiers()?,
9311                "fabric" => node.fabric_ref = self.consume_any_ident_or_kw()?.value,
9312                "region" => node.region = self.consume(TokenType::StringLit)?.value,
9313                "zones" => node.zones = self.parse_optional_int(),
9314                "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
9315                // v4.5.0 — the census a zip3 reduction relies on (axon-T1235).
9316                "census" => node.census = self.consume_any_ident_or_kw()?.value.clone(),
9317                _ => self.skip_value(),
9318            }
9319        }
9320        self.consume(TokenType::RBrace)?;
9321        Ok(node)
9322    }
9323
9324    /// Parse: `observe Name from Manifest { sources, quorum, timeout, on_partition, certainty_floor }`.
9325    fn parse_observe(&mut self) -> Result<ObserveDefinition, ParseError> {
9326        let tok = self.consume(TokenType::Observe)?;
9327        let name = self.consume(TokenType::Identifier)?.value;
9328        // `from <Manifest>` — required per Python grammar.
9329        self.consume(TokenType::From)?;
9330        let target = self.consume(TokenType::Identifier)?.value;
9331        let mut node = ObserveDefinition {
9332            name,
9333            target,
9334            sources: Vec::new(),
9335            quorum: None,
9336            timeout: String::new(),
9337            on_partition: "fail".to_string(),
9338            certainty_floor: None,
9339            loc: Loc {
9340                line: tok.line,
9341                column: tok.column,
9342            },
9343            leading_trivia: Vec::new(),
9344            trailing_trivia: Vec::new(),
9345        };
9346        self.consume(TokenType::LBrace)?;
9347        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9348            let field_name = self.current().value.clone();
9349            self.advance();
9350            if !self.check(TokenType::Colon) {
9351                if self.check(TokenType::LBrace) {
9352                    self.skip_braced_block()?;
9353                }
9354                continue;
9355            }
9356            self.advance();
9357            match field_name.as_str() {
9358                "sources" => node.sources = self.parse_bracketed_identifiers()?,
9359                "quorum" => node.quorum = self.parse_optional_int(),
9360                "timeout" => {
9361                    let t = self.current().clone();
9362                    match t.ttype {
9363                        TokenType::Duration | TokenType::StringLit => {
9364                            self.advance();
9365                            node.timeout = t.value;
9366                        }
9367                        _ => node.timeout = self.consume_any_ident_or_kw()?.value,
9368                    }
9369                }
9370                "on_partition" => {
9371                    let p_tok = self.consume_any_ident_or_kw()?;
9372                    let p = p_tok.value;
9373                    if !matches!(p.as_str(), "fail" | "shield_quarantine") {
9374                        return Err(ParseError {
9375                            message: format!(
9376                                "Invalid on_partition '{p}' in observe '{}' — \
9377                                 expected fail | shield_quarantine",
9378                                node.name
9379                            ),
9380                            line: p_tok.line,
9381                            column: p_tok.column,
9382                                                    ..Default::default()
9383                        });
9384                    }
9385                    node.on_partition = p;
9386                }
9387                "certainty_floor" => node.certainty_floor = self.parse_optional_float(),
9388                _ => self.skip_value(),
9389            }
9390        }
9391        self.consume(TokenType::RBrace)?;
9392        Ok(node)
9393    }
9394
9395    // ── v1.1.0 — Control cognitivo ─────────────────────────
9396
9397    /// Parse: `reconcile Name { observe, threshold, tolerance, on_drift, shield, mandate, max_retries }`.
9398    fn parse_reconcile(&mut self) -> Result<ReconcileDefinition, ParseError> {
9399        let tok = self.consume(TokenType::Reconcile)?;
9400        let name = self.consume(TokenType::Identifier)?.value;
9401        let mut node = ReconcileDefinition {
9402            name,
9403            observe_ref: String::new(),
9404            threshold: None,
9405            tolerance: None,
9406            on_drift: "provision".to_string(),
9407            shield_ref: String::new(),
9408            mandate_ref: String::new(),
9409            max_retries: 3,
9410            loc: Loc {
9411                line: tok.line,
9412                column: tok.column,
9413            },
9414            leading_trivia: Vec::new(),
9415            trailing_trivia: Vec::new(),
9416        };
9417        self.consume(TokenType::LBrace)?;
9418        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9419            let field_name = self.current().value.clone();
9420            self.advance();
9421            if !self.check(TokenType::Colon) {
9422                if self.check(TokenType::LBrace) {
9423                    self.skip_braced_block()?;
9424                }
9425                continue;
9426            }
9427            self.advance();
9428            match field_name.as_str() {
9429                "observe" => node.observe_ref = self.consume_any_ident_or_kw()?.value,
9430                "threshold" => node.threshold = self.parse_optional_float(),
9431                "tolerance" => node.tolerance = self.parse_optional_float(),
9432                "on_drift" => {
9433                    let d_tok = self.consume_any_ident_or_kw()?;
9434                    let d = d_tok.value;
9435                    if !matches!(d.as_str(), "provision" | "alert" | "refine") {
9436                        return Err(ParseError {
9437                            message: format!(
9438                                "Invalid on_drift '{d}' in reconcile '{}' — \
9439                                 expected provision | alert | refine",
9440                                node.name
9441                            ),
9442                            line: d_tok.line,
9443                            column: d_tok.column,
9444                                                    ..Default::default()
9445                        });
9446                    }
9447                    node.on_drift = d;
9448                }
9449                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
9450                "mandate" => node.mandate_ref = self.consume_any_ident_or_kw()?.value,
9451                "max_retries" => {
9452                    if let Some(v) = self.parse_optional_int() {
9453                        node.max_retries = v;
9454                    }
9455                }
9456                _ => self.skip_value(),
9457            }
9458        }
9459        self.consume(TokenType::RBrace)?;
9460        Ok(node)
9461    }
9462
9463    /// Parse: `lease Name { resource, duration, acquire, on_expire }`.
9464    fn parse_lease(&mut self) -> Result<LeaseDefinition, ParseError> {
9465        let tok = self.consume(TokenType::Lease)?;
9466        let name = self.consume(TokenType::Identifier)?.value;
9467        let mut node = LeaseDefinition {
9468            name,
9469            resource_ref: String::new(),
9470            duration: String::new(),
9471            acquire: "on_start".to_string(),
9472            on_expire: "anchor_breach".to_string(),
9473            loc: Loc {
9474                line: tok.line,
9475                column: tok.column,
9476            },
9477            leading_trivia: Vec::new(),
9478            trailing_trivia: Vec::new(),
9479        };
9480        self.consume(TokenType::LBrace)?;
9481        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9482            let field_name = self.current().value.clone();
9483            self.advance();
9484            if !self.check(TokenType::Colon) {
9485                if self.check(TokenType::LBrace) {
9486                    self.skip_braced_block()?;
9487                }
9488                continue;
9489            }
9490            self.advance();
9491            match field_name.as_str() {
9492                "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
9493                "duration" => {
9494                    let t = self.current().clone();
9495                    match t.ttype {
9496                        TokenType::Duration | TokenType::StringLit => {
9497                            self.advance();
9498                            node.duration = t.value;
9499                        }
9500                        _ => node.duration = self.consume_any_ident_or_kw()?.value,
9501                    }
9502                }
9503                "acquire" => {
9504                    let a_tok = self.consume_any_ident_or_kw()?;
9505                    let a = a_tok.value;
9506                    if !matches!(a.as_str(), "on_start" | "on_demand") {
9507                        return Err(ParseError {
9508                            message: format!(
9509                                "Invalid acquire '{a}' in lease '{}' — \
9510                                 expected on_start | on_demand",
9511                                node.name
9512                            ),
9513                            line: a_tok.line,
9514                            column: a_tok.column,
9515                                                    ..Default::default()
9516                        });
9517                    }
9518                    node.acquire = a;
9519                }
9520                "on_expire" => {
9521                    let e_tok = self.consume_any_ident_or_kw()?;
9522                    let e = e_tok.value;
9523                    if !matches!(e.as_str(), "anchor_breach" | "release" | "extend") {
9524                        return Err(ParseError {
9525                            message: format!(
9526                                "Invalid on_expire '{e}' in lease '{}' — \
9527                                 expected anchor_breach | release | extend",
9528                                node.name
9529                            ),
9530                            line: e_tok.line,
9531                            column: e_tok.column,
9532                                                    ..Default::default()
9533                        });
9534                    }
9535                    node.on_expire = e;
9536                }
9537                _ => self.skip_value(),
9538            }
9539        }
9540        self.consume(TokenType::RBrace)?;
9541        Ok(node)
9542    }
9543
9544    /// Parse: `ensemble Name { observations, quorum, aggregation, certainty_mode }`.
9545    fn parse_ensemble(&mut self) -> Result<EnsembleDefinition, ParseError> {
9546        let tok = self.consume(TokenType::Ensemble)?;
9547        let name = self.consume(TokenType::Identifier)?.value;
9548        let mut node = EnsembleDefinition {
9549            name,
9550            observations: Vec::new(),
9551            quorum: None,
9552            aggregation: "majority".to_string(),
9553            certainty_mode: "min".to_string(),
9554            loc: Loc {
9555                line: tok.line,
9556                column: tok.column,
9557            },
9558            leading_trivia: Vec::new(),
9559            trailing_trivia: Vec::new(),
9560        };
9561        self.consume(TokenType::LBrace)?;
9562        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9563            let field_name = self.current().value.clone();
9564            self.advance();
9565            if !self.check(TokenType::Colon) {
9566                if self.check(TokenType::LBrace) {
9567                    self.skip_braced_block()?;
9568                }
9569                continue;
9570            }
9571            self.advance();
9572            match field_name.as_str() {
9573                "observations" => node.observations = self.parse_bracketed_identifiers()?,
9574                "quorum" => node.quorum = self.parse_optional_int(),
9575                "aggregation" => {
9576                    let a_tok = self.consume_any_ident_or_kw()?;
9577                    let a = a_tok.value;
9578                    if !matches!(a.as_str(), "majority" | "weighted" | "byzantine") {
9579                        return Err(ParseError {
9580                            message: format!(
9581                                "Invalid aggregation '{a}' in ensemble '{}' — \
9582                                 expected majority | weighted | byzantine",
9583                                node.name
9584                            ),
9585                            line: a_tok.line,
9586                            column: a_tok.column,
9587                                                    ..Default::default()
9588                        });
9589                    }
9590                    node.aggregation = a;
9591                }
9592                "certainty_mode" => {
9593                    let c_tok = self.consume_any_ident_or_kw()?;
9594                    let c = c_tok.value;
9595                    if !matches!(c.as_str(), "min" | "weighted" | "harmonic") {
9596                        return Err(ParseError {
9597                            message: format!(
9598                                "Invalid certainty_mode '{c}' in ensemble '{}' — \
9599                                 expected min | weighted | harmonic",
9600                                node.name
9601                            ),
9602                            line: c_tok.line,
9603                            column: c_tok.column,
9604                                                    ..Default::default()
9605                        });
9606                    }
9607                    node.certainty_mode = c;
9608                }
9609                _ => self.skip_value(),
9610            }
9611        }
9612        self.consume(TokenType::RBrace)?;
9613        Ok(node)
9614    }
9615
9616    // ── v1.1.0 — Topology + π-calculus binary sessions ─────
9617
9618    /// Parse: `session Name { role1: [step, …]  role2: [step, …] }`.
9619    ///
9620    /// The enclosing `parse_session_definition` disambiguates from the session
9621    /// step token `session` (which does not exist) by always entering from the
9622    /// top-level dispatch; the identifier role name is consumed after `{`.
9623    fn parse_session_definition(&mut self) -> Result<SessionDefinition, ParseError> {
9624        let tok = self.consume(TokenType::Session)?;
9625        let name = self.consume(TokenType::Identifier)?.value;
9626        let mut node = SessionDefinition {
9627            name,
9628            roles: Vec::new(),
9629            loc: Loc {
9630                line: tok.line,
9631                column: tok.column,
9632            },
9633            leading_trivia: Vec::new(),
9634            trailing_trivia: Vec::new(),
9635        };
9636        self.consume(TokenType::LBrace)?;
9637        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9638            let role_tok = self.consume_any_ident_or_kw()?;
9639            self.consume(TokenType::Colon)?;
9640            let steps = self.parse_session_steps()?;
9641            node.roles.push(SessionRole {
9642                name: role_tok.value,
9643                steps,
9644                loc: Loc {
9645                    line: role_tok.line,
9646                    column: role_tok.column,
9647                },
9648            });
9649        }
9650        self.consume(TokenType::RBrace)?;
9651        Ok(node)
9652    }
9653
9654    /// v2.4.0 — Parse a Pauli-sum observable declaration:
9655    /// ```text
9656    /// observable EnergyHamiltonian {
9657    ///     qubits: 2
9658    ///     term: 0.5 * "ZZ"
9659    ///     term: -1.2 * "XI"
9660    /// }
9661    /// ```
9662    /// `term:` is a repeatable key (one `cₖ · Pₖ` per line). The coefficient is
9663    /// a real scalar (optional leading `+`/`-`), then `*`, then a quoted Pauli
9664    /// string. The type-checker (v2.4.0) validates the closed `{I,X,Y,Z}`
9665    /// alphabet + equal lengths; real coefficients ⇒ Hermitian by construction.
9666    fn parse_observable(&mut self) -> Result<ObservableDefinition, ParseError> {
9667        let tok = self.consume(TokenType::Observable)?;
9668        let name = self.consume(TokenType::Identifier)?.value;
9669        let mut node = ObservableDefinition {
9670            name,
9671            qubits: None,
9672            terms: Vec::new(),
9673            loc: Loc {
9674                line: tok.line,
9675                column: tok.column,
9676            },
9677            leading_trivia: Vec::new(),
9678            trailing_trivia: Vec::new(),
9679        };
9680        self.consume(TokenType::LBrace)?;
9681        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9682            let key_tok = self.consume_any_ident_or_kw()?;
9683            self.consume(TokenType::Colon)?;
9684            match key_tok.value.as_str() {
9685                "qubits" => node.qubits = Some(self.consume_number()? as i64),
9686                "term" => {
9687                    let term_loc = Loc {
9688                        line: key_tok.line,
9689                        column: key_tok.column,
9690                    };
9691                    // Optional sign, then magnitude.
9692                    let mut negative = false;
9693                    if self.check(TokenType::Minus) {
9694                        self.advance();
9695                        negative = true;
9696                    } else if self.check(TokenType::Plus) {
9697                        self.advance();
9698                    }
9699                    let mag = self.consume_number()?;
9700                    let coefficient = if negative { -mag } else { mag };
9701                    // `*` separator between coefficient and Pauli string.
9702                    self.consume(TokenType::Star)?;
9703                    let pauli = self.consume(TokenType::StringLit)?.value;
9704                    node.terms.push(PauliTerm {
9705                        coefficient,
9706                        pauli,
9707                        loc: term_loc,
9708                    });
9709                }
9710                _ => self.skip_value(),
9711            }
9712        }
9713        self.consume(TokenType::RBrace)?;
9714        Ok(node)
9715    }
9716
9717    /// v2.23.0 — Parse:
9718    /// `witness Name { claim: <ref>  against: <baseline>  metric: <metric>
9719    ///                 threshold: <ε>  data: <source> }`.
9720    /// Order-free `key: value` pairs. `claim`/`against`/`metric`/`data` are bare
9721    /// identifiers (a ref or a closed-catalog keyword); `threshold` is a number.
9722    /// Well-formedness (known metric, threshold range, required fields) is the
9723    /// type-checker's job (`axon-E0790`).
9724    fn parse_witness(&mut self) -> Result<WitnessDefinition, ParseError> {
9725        let tok = self.consume(TokenType::Witness)?;
9726        let name = self.consume(TokenType::Identifier)?.value;
9727        let mut node = WitnessDefinition {
9728            name,
9729            claim: String::new(),
9730            baseline: String::new(),
9731            metric: String::new(),
9732            threshold: 0.0,
9733            data: String::new(),
9734            loc: Loc {
9735                line: tok.line,
9736                column: tok.column,
9737            },
9738            leading_trivia: Vec::new(),
9739            trailing_trivia: Vec::new(),
9740        };
9741        self.consume(TokenType::LBrace)?;
9742        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9743            let key_tok = self.consume_any_ident_or_kw()?;
9744            self.consume(TokenType::Colon)?;
9745            match key_tok.value.as_str() {
9746                "claim" => node.claim = self.consume_any_ident_or_kw()?.value,
9747                // `against` is the baseline; `against` is not a reserved keyword,
9748                // so it lexes as an identifier key here.
9749                "against" => node.baseline = self.consume_any_ident_or_kw()?.value,
9750                "metric" => node.metric = self.consume_any_ident_or_kw()?.value,
9751                "threshold" => node.threshold = self.consume_number()?,
9752                "data" => node.data = self.consume_any_ident_or_kw()?.value,
9753                _ => self.skip_value(),
9754            }
9755        }
9756        self.consume(TokenType::RBrace)?;
9757        Ok(node)
9758    }
9759
9760    /// v2.3.0 — Parse:
9761    /// `socket Name { protocol: SessionRef, backpressure: credit(n),
9762    ///               reconnect: cognitive_state, legal_basis: ... }`.
9763    /// Fields are `key: value` pairs (order-free); only `protocol` is required.
9764    fn parse_socket(&mut self) -> Result<SocketDefinition, ParseError> {
9765        let tok = self.consume(TokenType::Socket)?;
9766        let name = self.consume(TokenType::Identifier)?.value;
9767        let mut node = SocketDefinition {
9768            name,
9769            loc: Loc { line: tok.line, column: tok.column },
9770            ..Default::default()
9771        };
9772        self.consume(TokenType::LBrace)?;
9773        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9774            let key = self.consume_any_ident_or_kw()?.value;
9775            self.consume(TokenType::Colon)?;
9776            match key.as_str() {
9777                "protocol" => node.protocol = self.consume_any_ident_or_kw()?.value,
9778                "backpressure" => {
9779                    // `credit(n)` — the typed-resource window.
9780                    let kind = self.consume_any_ident_or_kw()?.value;
9781                    if kind != "credit" {
9782                        return Err(self.error(&format!("expected `credit(n)` for backpressure, got `{kind}`")));
9783                    }
9784                    self.consume(TokenType::LParen)?;
9785                    let n = self
9786                        .consume(TokenType::Integer)?
9787                        .value
9788                        .parse::<i64>()
9789                        .map_err(|_| self.error("backpressure credit must be an integer"))?;
9790                    self.consume(TokenType::RParen)?;
9791                    node.backpressure_credit = Some(n);
9792                }
9793                "reconnect" => {
9794                    let mode = self.consume_any_ident_or_kw()?.value;
9795                    node.reconnect = mode == "cognitive_state";
9796                }
9797                "legal_basis" => node.legal_basis = Some(self.consume_any_ident_or_kw()?.value),
9798                other => return Err(self.error(&format!("unknown socket field `{other}`"))),
9799            }
9800            // Optional comma between fields.
9801            if self.check(TokenType::Comma) {
9802                self.consume(TokenType::Comma)?;
9803            }
9804        }
9805        self.consume(TokenType::RBrace)?;
9806        Ok(node)
9807    }
9808
9809    /// v2.37.0 — parse `upstream Name [from Preset@vN] { fields }`.
9810    ///
9811    /// Field grammar per `the design plan` section 1–2. The
9812    /// parser fixes the *shape* only; catalog membership (`transport:`,
9813    /// `auth:`, `overflow:`, `on_exhausted:`), key charsets and projection
9814    /// totality are v2.37.0 type-checker laws (T849–T851), mirroring how
9815    /// `socket` splits parse vs. check.
9816    fn parse_upstream(&mut self) -> Result<UpstreamDefinition, ParseError> {
9817        let tok = self.consume(TokenType::Upstream)?;
9818        let name = self.consume(TokenType::Identifier)?.value;
9819        let mut node = UpstreamDefinition {
9820            name,
9821            loc: Loc { line: tok.line, column: tok.column },
9822            ..Default::default()
9823        };
9824        // v2.37.0 — preset instantiation: `upstream X from DeepgramSTT@v1 {…}`.
9825        if self.check(TokenType::From) {
9826            self.advance();
9827            let base = self.consume(TokenType::Identifier)?.value;
9828            self.consume(TokenType::At)?;
9829            let version = self.consume_any_ident_or_kw()?.value;
9830            node.preset = Some(format!("{base}@{version}"));
9831        }
9832        self.consume(TokenType::LBrace)?;
9833        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9834            let key = self.consume_any_ident_or_kw()?.value;
9835            self.consume(TokenType::Colon)?;
9836            match key.as_str() {
9837                "transport" => node.transport = self.consume_any_ident_or_kw()?.value,
9838                "protocol" => node.protocol = self.consume_any_ident_or_kw()?.value,
9839                "role" => node.role = self.consume_any_ident_or_kw()?.value,
9840                "resolve" => node.resolve = self.parse_dotted_identifier()?,
9841                // v2.69.0 — the upstream's channel rides a declared
9842                // `resource`; the address + instance bound DERIVE from it.
9843                // XOR with `resolve:` is axon-T951 (type-checker territory).
9844                "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
9845                "secret" => node.secret = self.parse_dotted_identifier()?,
9846                "auth" => {
9847                    // `header("Name")` | `header("Name", "Prefix ")` |
9848                    // `query("param")` | `signed_url`.
9849                    node.auth_kind = self.consume_any_ident_or_kw()?.value;
9850                    if self.check(TokenType::LParen) {
9851                        self.consume(TokenType::LParen)?;
9852                        node.auth_name = Some(self.consume(TokenType::StringLit)?.value);
9853                        if self.check(TokenType::Comma) {
9854                            self.consume(TokenType::Comma)?;
9855                            node.auth_prefix = Some(self.consume(TokenType::StringLit)?.value);
9856                        }
9857                        self.consume(TokenType::RParen)?;
9858                    }
9859                }
9860                "map" => node.map = self.parse_upstream_map()?,
9861                "reconnect" => node.reconnect = Some(self.parse_upstream_reconnect()?),
9862                "overflow" => node.overflow = Some(self.consume_any_ident_or_kw()?.value),
9863                "backpressure" => {
9864                    // `credit(n)` — same typed-resource window as `socket`.
9865                    let kind = self.consume_any_ident_or_kw()?.value;
9866                    if kind != "credit" {
9867                        return Err(self.error(&format!("expected `credit(n)` for backpressure, got `{kind}`")));
9868                    }
9869                    self.consume(TokenType::LParen)?;
9870                    let n = self
9871                        .consume(TokenType::Integer)?
9872                        .value
9873                        .parse::<i64>()
9874                        .map_err(|_| self.error("backpressure credit must be an integer"))?;
9875                    self.consume(TokenType::RParen)?;
9876                    node.backpressure_credit = Some(n);
9877                }
9878                other => return Err(self.error(&format!("unknown upstream field `{other}`"))),
9879            }
9880            // Optional comma between fields.
9881            if self.check(TokenType::Comma) {
9882                self.consume(TokenType::Comma)?;
9883            }
9884        }
9885        self.consume(TokenType::RBrace)?;
9886        Ok(node)
9887    }
9888
9889    /// v2.38.0 — parse `cors Name { fields }`. Field-shape checks
9890    /// (wildcard+credentials, origin-glob shape, closed method catalog,
9891    /// cross-method path consistency) are v2.38.0 type-checker territory
9892    /// (T853-T857); the parser only builds the structural AST.
9893    ///
9894    /// **Unknown fields are a hard error** (the design decision, not `shield`'s lenient
9895    /// `axon-W010` record-and-skip) — mirrors `upstream`'s stricter
9896    /// posture, appropriate for a security-relevant declaration.
9897    fn parse_cors(&mut self) -> Result<CorsDefinition, ParseError> {
9898        let tok = self.consume(TokenType::Cors)?;
9899        let name = self.consume(TokenType::Identifier)?.value;
9900        let mut node = CorsDefinition {
9901            name,
9902            loc: Loc { line: tok.line, column: tok.column },
9903            ..Default::default()
9904        };
9905        self.consume(TokenType::LBrace)?;
9906        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9907            let key = self.consume_any_ident_or_kw()?.value;
9908            self.consume(TokenType::Colon)?;
9909            match key.as_str() {
9910                "allow_origins" => node.allow_origins = self.parse_bracketed_strings()?,
9911                "allow_methods" => node.allow_methods = self.parse_bracketed_identifiers()?,
9912                "allow_headers" => node.allow_headers = self.parse_bracketed_strings()?,
9913                "allow_credentials" => {
9914                    node.allow_credentials = self.consume_any_ident_or_kw()?.value == "true"
9915                }
9916                "max_age" => node.max_age = Some(self.consume(TokenType::Duration)?.value),
9917                "expose_headers" => node.expose_headers = self.parse_bracketed_strings()?,
9918                other => return Err(self.error(&format!("unknown cors field `{other}`"))),
9919            }
9920            // Optional comma between fields.
9921            if self.check(TokenType::Comma) {
9922                self.consume(TokenType::Comma)?;
9923            }
9924        }
9925        self.consume(TokenType::RBrace)?;
9926        Ok(node)
9927    }
9928
9929    /// v2.46.0 — parse `credential Name { ttl: grants: }`. Strict
9930    /// closed-catalog (unknown field is a hard error, the v2.38.0 the design decision
9931    /// discipline — a credential contract governs AUTHORITY, so a typo can
9932    /// never silently produce a permissive contract). `grants:` slugs are
9933    /// validated at parse time with the same closed grammar as
9934    /// `axonendpoint requires:`; the cross-field laws (non-empty grants,
9935    /// TTL bounds) are v2.46.0 type-checker territory (`axon-T893`/`T894`).
9936    fn parse_credential(&mut self) -> Result<CredentialDefinition, ParseError> {
9937        let tok = self.consume(TokenType::Credential)?;
9938        let name = self.consume(TokenType::Identifier)?.value;
9939        let mut node = CredentialDefinition {
9940            name,
9941            loc: Loc { line: tok.line, column: tok.column },
9942            ..Default::default()
9943        };
9944        self.consume(TokenType::LBrace)?;
9945        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9946            let key = self.consume_any_ident_or_kw()?.value;
9947            self.consume(TokenType::Colon)?;
9948            match key.as_str() {
9949                "ttl" => node.ttl = self.consume(TokenType::Duration)?.value,
9950                "grants" => {
9951                    let bracket_tok = self.current().clone();
9952                    let items = self.parse_bracketed_dot_identifiers()?;
9953                    for slug in &items {
9954                        if !is_valid_capability_slug(slug) {
9955                            return Err(ParseError {
9956                                message: format!(
9957                                    "Invalid capability slug '{slug}' in credential '{}' \
9958                                     `grants:`. Capability slugs must match \
9959                                     ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
9960                                     lowercase identifiers starting with a letter. Examples: \
9961                                     `chat.invoke`, `flow.execute`.",
9962                                    node.name
9963                                ),
9964                                line: bracket_tok.line,
9965                                column: bracket_tok.column,
9966                                ..Default::default()
9967                            });
9968                        }
9969                    }
9970                    node.grants = items;
9971                }
9972                other => return Err(self.error(&format!("unknown credential field `{other}`"))),
9973            }
9974            // Optional comma between fields.
9975            if self.check(TokenType::Comma) {
9976                self.consume(TokenType::Comma)?;
9977            }
9978        }
9979        self.consume(TokenType::RBrace)?;
9980        Ok(node)
9981    }
9982
9983    /// v2.40.0 — parse `cache Name { backend:, ttl:, key:, default:,
9984    /// apply_to_effects:, invalidate_on: }`. Strict closed-catalog (unknown
9985    /// field is a hard error, the v2.38.0 the design decision discipline — a cache governs
9986    /// correctness, so a typo can never silently mean "no policy"). All
9987    /// cross-field laws (single default, non-pure-needs-ttl, reference
9988    /// resolution, effect widening) are v2.40.0 type-checker territory.
9989    fn parse_cache(&mut self) -> Result<CacheDefinition, ParseError> {
9990        let tok = self.consume(TokenType::Cache)?;
9991        let name = self.consume(TokenType::Identifier)?.value;
9992        let mut node = CacheDefinition {
9993            name,
9994            loc: Loc { line: tok.line, column: tok.column },
9995            ..Default::default()
9996        };
9997        self.consume(TokenType::LBrace)?;
9998        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9999            let key = self.consume_any_ident_or_kw()?.value;
10000            self.consume(TokenType::Colon)?;
10001            match key.as_str() {
10002                "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
10003                "ttl" => node.ttl = Some(self.consume(TokenType::Duration)?.value),
10004                "key" => node.key_params = self.parse_bracketed_identifiers()?,
10005                "default" => {
10006                    node.default_policy = self.consume_any_ident_or_kw()?.value == "true"
10007                }
10008                "apply_to_effects" => {
10009                    node.apply_to_effects = self.parse_bracketed_identifiers()?
10010                }
10011                "invalidate_on" => node.invalidate_on = self.parse_bracketed_identifiers()?,
10012                other => return Err(self.error(&format!("unknown cache field `{other}`"))),
10013            }
10014            if self.check(TokenType::Comma) {
10015                self.consume(TokenType::Comma)?;
10016            }
10017        }
10018        self.consume(TokenType::RBrace)?;
10019        Ok(node)
10020    }
10021
10022    // ── v2.53.0 — Native Document Synthesis ─────────────────────────────
10023
10024    /// v2.53.0 — parse `document <Name> { target:, template:?, provenance:?,
10025    /// effects:?, <body blocks> }`. Document-level scalars are handled here;
10026    /// anything of the form `ident { … }` is a body block ([`parse_doc_block_body`]).
10027    /// Unknown scalar fields are a hard error (the v2.38.0/v2.39.0 closed-catalog
10028    /// discipline); the per-`target` block vocabulary is the v2.53.0 checker's job.
10029    fn parse_document(&mut self) -> Result<crate::ast::DocumentDefinition, ParseError> {
10030        let tok = self.consume(TokenType::Document)?;
10031        let name = self.consume(TokenType::Identifier)?.value;
10032        let mut node = crate::ast::DocumentDefinition {
10033            name,
10034            loc: Loc {
10035                line: tok.line,
10036                column: tok.column,
10037            },
10038            ..Default::default()
10039        };
10040        self.consume(TokenType::LBrace)?;
10041        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10042            let field = self.current().clone();
10043            let field_name = field.value.clone();
10044            self.advance();
10045            if self.check(TokenType::Colon) {
10046                self.advance();
10047                match field_name.as_str() {
10048                    "target" => node.target = self.consume_any_ident_or_kw()?.value,
10049                    "template" => node.template = self.parse_dotted_identifier()?,
10050                    "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
10051                    // v4.4.0 — the typed input the coverage rule reads. It names a
10052                    // DECLARED TYPE, not a value: a sink whose input has no type has
10053                    // nowhere to read a regulatory class from.
10054                    "payload" => node.payload = self.consume_any_ident_or_kw()?.value,
10055                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
10056                    "effects" => node.effects = Some(self.parse_effect_row()?),
10057                    other => {
10058                        return Err(self.error(&format!(
10059                            "unknown document field `{other}` in document `{}` — expected \
10060                             `target:` / `template:` / `provenance:` / `effects:`, or a body \
10061                             block (`section {{ … }}` / `slide {{ … }}` / `sheet {{ … }}`)",
10062                            node.name
10063                        )))
10064                    }
10065                }
10066            } else if self.check(TokenType::LBrace) {
10067                node.blocks
10068                    .push(self.parse_doc_block_body(field_name, field.line, field.column)?);
10069            } else {
10070                return Err(self.error(&format!(
10071                    "unexpected `{field_name}` in document `{}` body — expected a `field:` or a \
10072                     body block `{field_name} {{ … }}`",
10073                    node.name
10074                )));
10075            }
10076            if self.check(TokenType::Comma) {
10077                self.advance();
10078            }
10079        }
10080        self.consume(TokenType::RBrace)?;
10081        Ok(node)
10082    }
10083
10084    /// v2.53.0 — parse a document body block whose `kind` was already
10085    /// consumed: `{ (field: value | nested-block { … })* }`. Recursive — a
10086    /// `section` holds `para`/`table`/`chart`; a `slide` holds `bullets`/
10087    /// `notes`; a `sheet` holds `row`/`formula`. A member is a field iff a
10088    /// `:` follows its name; else it must open a nested block (`{`).
10089    fn parse_doc_block_body(
10090        &mut self,
10091        kind: String,
10092        line: u32,
10093        column: u32,
10094    ) -> Result<crate::ast::DocBlock, ParseError> {
10095        let mut block = crate::ast::DocBlock {
10096            kind,
10097            loc: Loc { line, column },
10098            ..Default::default()
10099        };
10100        self.consume(TokenType::LBrace)?;
10101        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10102            let name_tok = self.current().clone();
10103            let name = self.consume_any_ident_or_kw()?.value;
10104            if self.check(TokenType::Colon) {
10105                self.advance();
10106                let value = self.parse_doc_scalar()?;
10107                block.fields.push((name, value));
10108            } else if self.check(TokenType::LBrace) {
10109                let child = self.parse_doc_block_body(name, name_tok.line, name_tok.column)?;
10110                block.children.push(child);
10111            } else {
10112                return Err(self.error(&format!(
10113                    "in document block `{}`: `{name}` must be a `field:` value or open a nested \
10114                     block `{name} {{ … }}`",
10115                    block.kind
10116                )));
10117            }
10118            if self.check(TokenType::Comma) {
10119                self.advance();
10120            }
10121        }
10122        self.consume(TokenType::RBrace)?;
10123        Ok(block)
10124    }
10125
10126    /// v2.53.0 — parse a document field value into a [`crate::ast::DocScalar`].
10127    /// A bare identifier is a REFERENCE (`text: revenue_summary`) — this is what
10128    /// the assertion-laundering barrier inspects; a quoted string / int / bool /
10129    /// bracketed list are literals.
10130    fn parse_doc_scalar(&mut self) -> Result<crate::ast::DocScalar, ParseError> {
10131        let tok = self.current().clone();
10132        match tok.ttype {
10133            TokenType::StringLit => {
10134                self.advance();
10135                Ok(crate::ast::DocScalar::Text(tok.value))
10136            }
10137            TokenType::Integer => {
10138                self.advance();
10139                Ok(crate::ast::DocScalar::Int(tok.value.parse::<i64>().unwrap_or(0)))
10140            }
10141            TokenType::Bool => {
10142                self.advance();
10143                Ok(crate::ast::DocScalar::Bool(tok.value == "true"))
10144            }
10145            TokenType::LBracket => {
10146                let items = self.parse_bracketed_strings()?;
10147                Ok(crate::ast::DocScalar::List(items))
10148            }
10149            _ => {
10150                let name = self.consume_any_ident_or_kw()?.value;
10151                Ok(crate::ast::DocScalar::Ref(name))
10152            }
10153        }
10154    }
10155
10156    // ── v2.60.0 — Governed CRM Delivery ──────────────────────────────────
10157
10158    /// v2.60.0 — parse `deliver <Name> { target:, provenance:?, secret:,
10159    /// effects:?, <operation blocks> }`. Delivery-level scalars are handled here;
10160    /// anything of the form `ident { … }` is an operation block
10161    /// ([`parse_deliver_op`]). Unknown scalar fields are a hard error (the v2.53.0
10162    /// v2.66.0 — the governed human-notification declaration:
10163    ///
10164    /// ```text
10165    /// notify LowSales {
10166    ///     channel:    sms | whatsapp | telegram
10167    ///     to:         secret(ops.oncall_phone)
10168    ///     template:   "Ventas 7d: ${resumen}"
10169    ///     window:     4h
10170    ///     provenance: attached | cleared
10171    ///     effects:    <web>
10172    /// }
10173    /// ```
10174    ///
10175    /// The closed-field discipline (v2.53.0/v2.60.0): an unknown scalar field is
10176    /// a hard parse error. The LAWS (T933/T934/T935) live in the checker
10177    /// so violations accumulate; the parser records shape (including a
10178    /// literal `to:` — kept so T934 can refuse it TEACHING the custody
10179    /// form, instead of a bare parse error).
10180    fn parse_notify(&mut self) -> Result<crate::ast::NotifyDefinition, ParseError> {
10181        let tok = self.consume(TokenType::Notify)?;
10182        let name = self.consume(TokenType::Identifier)?.value;
10183        let mut node = crate::ast::NotifyDefinition {
10184            name,
10185            loc: Loc {
10186                line: tok.line,
10187                column: tok.column,
10188            },
10189            ..Default::default()
10190        };
10191        self.consume(TokenType::LBrace)?;
10192        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10193            let field = self.current().clone();
10194            let field_name = field.value.clone();
10195            self.advance();
10196            if self.check(TokenType::Colon) {
10197                self.advance();
10198                match field_name.as_str() {
10199                    "channel" => node.channel = self.consume_any_ident_or_kw()?.value,
10200                    "to" => {
10201                        // The custody form: `secret(<dotted-class>)`. A string
10202                        // literal parses too — the checker refuses it (T934)
10203                        // with the teaching message.
10204                        if self.current().value == "secret" && self.peek_is_lparen() {
10205                            self.advance(); // `secret`
10206                            self.consume(TokenType::LParen)?;
10207                            node.to_secret = self.parse_dotted_identifier()?;
10208                            self.consume(TokenType::RParen)?;
10209                            node.to_is_secret = true;
10210                        } else if self.check(TokenType::StringLit) {
10211                            node.to_secret = self.consume(TokenType::StringLit)?.value.clone();
10212                            node.to_is_secret = false;
10213                        } else {
10214                            node.to_secret = self.consume_any_ident_or_kw()?.value.clone();
10215                            node.to_is_secret = false;
10216                        }
10217                    }
10218                    "template" => {
10219                        node.template = self.consume(TokenType::StringLit)?.value.clone()
10220                    }
10221                    "window" => {
10222                        // `4h` lexes as Integer + ident or one ident — accept
10223                        // both spellings, normalized to the joined form.
10224                        if self.check(TokenType::Integer) {
10225                            let n = self.consume(TokenType::Integer)?.value.clone();
10226                            let unit = self.consume_any_ident_or_kw()?.value.clone();
10227                            node.window = format!("{n}{unit}");
10228                        } else {
10229                            node.window = self.consume_any_ident_or_kw()?.value.clone();
10230                        }
10231                    }
10232                    "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
10233                    // v4.4.0 — the typed input the coverage rule reads. It names a
10234                    // DECLARED TYPE, not a value: a sink whose input has no type has
10235                    // nowhere to read a regulatory class from.
10236                    "payload" => node.payload = self.consume_any_ident_or_kw()?.value,
10237                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
10238                    "effects" => node.effects = Some(self.parse_effect_row()?),
10239                    other => {
10240                        return Err(self.error(&format!(
10241                            "unknown notify field `{other}` in notify `{}` — expected \
10242                             `channel:` / `to:` / `template:` / `window:` / `provenance:` / \
10243                             `effects:`",
10244                            node.name
10245                        )))
10246                    }
10247                }
10248            }
10249        }
10250        self.consume(TokenType::RBrace)?;
10251        Ok(node)
10252    }
10253
10254    /// v2.66.0 — one-token lookahead helper for the `secret(` form.
10255    /// v2.69.0 — is the NEXT token an identifier? (`budget <Name> { … }` vs
10256    /// a bare `budget` used as an ordinary identifier.)
10257    fn peek_is_identifier(&self) -> bool {
10258        self.tokens
10259            .get(self.pos + 1)
10260            .map(|t| t.ttype == TokenType::Identifier)
10261            .unwrap_or(false)
10262    }
10263
10264    fn peek_is_lparen(&self) -> bool {
10265        self.tokens
10266            .get(self.pos + 1)
10267            .map(|t| t.ttype == TokenType::LParen)
10268            .unwrap_or(false)
10269    }
10270
10271    /// closed-catalog discipline); the operation vocabulary is the checker's job.
10272    fn parse_deliver(&mut self) -> Result<crate::ast::DeliverDefinition, ParseError> {
10273        let tok = self.consume(TokenType::Deliver)?;
10274        let name = self.consume(TokenType::Identifier)?.value;
10275        let mut node = crate::ast::DeliverDefinition {
10276            name,
10277            loc: Loc {
10278                line: tok.line,
10279                column: tok.column,
10280            },
10281            ..Default::default()
10282        };
10283        self.consume(TokenType::LBrace)?;
10284        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10285            let field = self.current().clone();
10286            let field_name = field.value.clone();
10287            self.advance();
10288            if self.check(TokenType::Colon) {
10289                self.advance();
10290                match field_name.as_str() {
10291                    "target" => node.target = self.consume_any_ident_or_kw()?.value,
10292                    "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
10293                    // v4.4.0 — the typed input the coverage rule reads. It names a
10294                    // DECLARED TYPE, not a value: a sink whose input has no type has
10295                    // nowhere to read a regulatory class from.
10296                    "payload" => node.payload = self.consume_any_ident_or_kw()?.value,
10297                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
10298                    "secret" => node.secret = self.consume_any_ident_or_kw()?.value,
10299                    "effects" => node.effects = Some(self.parse_effect_row()?),
10300                    other => {
10301                        return Err(self.error(&format!(
10302                            "unknown deliver field `{other}` in deliver `{}` — expected \
10303                             `target:` / `provenance:` / `secret:` / `effects:`, or an operation \
10304                             block (`upsert_contact {{ … }}` / `create_deal {{ … }}` / \
10305                             `add_note {{ … }}`)",
10306                            node.name
10307                        )))
10308                    }
10309                }
10310            } else if self.check(TokenType::LBrace) {
10311                node.ops
10312                    .push(self.parse_deliver_op(field_name, field.line, field.column)?);
10313            } else {
10314                return Err(self.error(&format!(
10315                    "unexpected `{field_name}` in deliver `{}` body — expected a `field:` or an \
10316                     operation block `{field_name} {{ … }}`",
10317                    node.name
10318                )));
10319            }
10320            if self.check(TokenType::Comma) {
10321                self.advance();
10322            }
10323        }
10324        self.consume(TokenType::RBrace)?;
10325        Ok(node)
10326    }
10327
10328    /// v2.60.0 — parse a delivery operation block whose `kind` was already
10329    /// consumed: `{ (field: value)* }`. Flat (unlike a document block, an
10330    /// operation has no nested children) — each member must be a `field: value`.
10331    fn parse_deliver_op(
10332        &mut self,
10333        kind: String,
10334        line: u32,
10335        column: u32,
10336    ) -> Result<crate::ast::DeliverOp, ParseError> {
10337        let mut op = crate::ast::DeliverOp {
10338            kind,
10339            loc: Loc { line, column },
10340            ..Default::default()
10341        };
10342        self.consume(TokenType::LBrace)?;
10343        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10344            let name = self.consume_any_ident_or_kw()?.value;
10345            self.consume(TokenType::Colon).map_err(|_| {
10346                self.error(&format!(
10347                    "in deliver operation `{}`: `{name}` must be a `field: value` pair — an \
10348                     operation binds scalar fields, it takes no nested blocks",
10349                    op.kind
10350                ))
10351            })?;
10352            let value = self.parse_doc_scalar()?;
10353            op.fields.push((name, value));
10354            if self.check(TokenType::Comma) {
10355                self.advance();
10356            }
10357        }
10358        self.consume(TokenType::RBrace)?;
10359        Ok(op)
10360    }
10361
10362    /// v2.42.0 — parse `savant <Name> { domain:, cognition{…}, memory{…},
10363    /// budget{…}, mandate <M> {…} … }`. The block surface only; catalog +
10364    /// ref-resolution + budget/interruptibility binding is the v2.42.0 checker's
10365    /// job (the standing parse/check split). Unknown fields are a hard error
10366    ///: a savant governs an expensive autonomous process.
10367    fn parse_savant(&mut self) -> Result<SavantDefinition, ParseError> {
10368        let tok = self.consume(TokenType::Savant)?;
10369        let name = self.consume(TokenType::Identifier)?.value;
10370        let mut node = SavantDefinition {
10371            name,
10372            loc: Loc {
10373                line: tok.line,
10374                column: tok.column,
10375            },
10376            ..Default::default()
10377        };
10378        self.consume(TokenType::LBrace)?;
10379        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10380            let field = self.current().clone();
10381            let field_name = field.value.clone();
10382            self.advance();
10383            if self.check(TokenType::Colon) {
10384                self.advance();
10385                match field_name.as_str() {
10386                    "domain" => node.domain = self.consume(TokenType::StringLit)?.value,
10387                    other => {
10388                        return Err(self.error(&format!(
10389                            "unknown savant field `{other}` in savant `{}` — expected \
10390                             `domain:` or a `cognition` / `memory` / `budget` / `mandate` block",
10391                            node.name
10392                        )))
10393                    }
10394                }
10395            } else if field_name == "cognition" {
10396                node.cognition = Some(self.parse_savant_cognition(field.line, field.column)?);
10397            } else if field_name == "memory" {
10398                node.memory = Some(self.parse_savant_memory(field.line, field.column)?);
10399            } else if field_name == "budget" {
10400                node.budget = Some(self.parse_savant_budget(field.line, field.column)?);
10401            } else if field_name == "mandate" {
10402                node.mandates
10403                    .push(self.parse_savant_mandate(field.line, field.column)?);
10404            } else {
10405                return Err(self.error(&format!(
10406                    "unexpected `{field_name}` in savant `{}` body — expected `domain:` or a \
10407                     `cognition` / `memory` / `budget` / `mandate` block",
10408                    node.name
10409                )));
10410            }
10411            if self.check(TokenType::Comma) {
10412                self.advance();
10413            }
10414        }
10415        self.consume(TokenType::RBrace)?;
10416        Ok(node)
10417    }
10418
10419    /// v2.42.0 — the `cognition { depth:, entropic_threshold:, divergence: }`
10420    /// sub-block. Catalog validation of `depth`/`divergence` is v2.42.0.
10421    fn parse_savant_cognition(
10422        &mut self,
10423        line: u32,
10424        column: u32,
10425    ) -> Result<SavantCognition, ParseError> {
10426        self.consume(TokenType::LBrace)?;
10427        let mut node = SavantCognition {
10428            loc: Loc { line, column },
10429            ..Default::default()
10430        };
10431        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10432            let key = self.consume_any_ident_or_kw()?.value;
10433            self.consume(TokenType::Colon)?;
10434            match key.as_str() {
10435                "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
10436                "entropic_threshold" => node.entropic_threshold = self.parse_optional_float(),
10437                "divergence" => node.divergence = self.consume_any_ident_or_kw()?.value,
10438                other => {
10439                    return Err(self.error(&format!(
10440                        "unknown savant `cognition` field `{other}` — expected \
10441                         `depth` / `entropic_threshold` / `divergence`"
10442                    )))
10443                }
10444            }
10445            if self.check(TokenType::Comma) {
10446                self.advance();
10447            }
10448        }
10449        self.consume(TokenType::RBrace)?;
10450        Ok(node)
10451    }
10452
10453    /// v2.42.0 — the `memory { backend:, corpus_graph:, isolation_level: }`
10454    /// sub-block. `backend` is resolved to a declared `memory`/`corpus` in v2.42.0.
10455    fn parse_savant_memory(
10456        &mut self,
10457        line: u32,
10458        column: u32,
10459    ) -> Result<SavantMemory, ParseError> {
10460        self.consume(TokenType::LBrace)?;
10461        let mut node = SavantMemory {
10462            loc: Loc { line, column },
10463            ..Default::default()
10464        };
10465        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10466            let key = self.consume_any_ident_or_kw()?.value;
10467            self.consume(TokenType::Colon)?;
10468            match key.as_str() {
10469                "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
10470                "corpus_graph" => {
10471                    node.corpus_graph = self.consume_any_ident_or_kw()?.value == "true"
10472                }
10473                "isolation_level" => node.isolation_level = self.consume_any_ident_or_kw()?.value,
10474                other => {
10475                    return Err(self.error(&format!(
10476                        "unknown savant `memory` field `{other}` — expected \
10477                         `backend` / `corpus_graph` / `isolation_level`"
10478                    )))
10479                }
10480            }
10481            if self.check(TokenType::Comma) {
10482                self.advance();
10483            }
10484        }
10485        self.consume(TokenType::RBrace)?;
10486        Ok(node)
10487    }
10488
10489    /// v2.42.0 — the `budget { max_iterations:, max_tool_synth: }` sub-block.
10490    /// Bound to a v2.28.0 linear budget (`RateLease`) in v2.42.0.
10491    fn parse_savant_budget(
10492        &mut self,
10493        line: u32,
10494        column: u32,
10495    ) -> Result<SavantBudget, ParseError> {
10496        self.consume(TokenType::LBrace)?;
10497        let mut node = SavantBudget {
10498            loc: Loc { line, column },
10499            ..Default::default()
10500        };
10501        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10502            let key = self.consume_any_ident_or_kw()?.value;
10503            self.consume(TokenType::Colon)?;
10504            match key.as_str() {
10505                "max_iterations" => node.max_iterations = self.parse_optional_int(),
10506                "max_tool_synth" => node.max_tool_synth = self.parse_optional_int(),
10507                other => {
10508                    return Err(self.error(&format!(
10509                        "unknown savant `budget` field `{other}` — expected \
10510                         `max_iterations` / `max_tool_synth`"
10511                    )))
10512                }
10513            }
10514            if self.check(TokenType::Comma) {
10515                self.advance();
10516            }
10517        }
10518        self.consume(TokenType::RBrace)?;
10519        Ok(node)
10520    }
10521
10522    /// v2.42.0 — the `mandate <Name> { objective:, output: }` sub-block. The
10523    /// `mandate` keyword is already consumed by `parse_savant`.
10524    fn parse_savant_mandate(
10525        &mut self,
10526        line: u32,
10527        column: u32,
10528    ) -> Result<SavantMandate, ParseError> {
10529        let name = self.consume(TokenType::Identifier)?.value;
10530        let mut node = SavantMandate {
10531            name,
10532            loc: Loc { line, column },
10533            ..Default::default()
10534        };
10535        self.consume(TokenType::LBrace)?;
10536        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10537            let key = self.consume_any_ident_or_kw()?.value;
10538            self.consume(TokenType::Colon)?;
10539            match key.as_str() {
10540                "objective" => node.objective = self.consume(TokenType::StringLit)?.value,
10541                "output" => node.output_type = self.consume_any_ident_or_kw()?.value,
10542                other => {
10543                    return Err(self.error(&format!(
10544                        "unknown savant `mandate` field `{other}` — expected `objective` / `output`"
10545                    )))
10546                }
10547            }
10548            if self.check(TokenType::Comma) {
10549                self.advance();
10550            }
10551        }
10552        self.consume(TokenType::RBrace)?;
10553        Ok(node)
10554    }
10555
10556    /// v2.42.0 — parse `synth <Name> { target:, risk:, language:, sandbox:,
10557    /// review:, max_lines: }`. Flat key:value block (the `cache` shape). Catalog
10558    /// + deny-by-default validation is v2.42.0 `check_synth`. Unknown fields are a
10559    /// hard error: a synth policy governs arbitrary-code execution.
10560    fn parse_synth(&mut self) -> Result<SynthDefinition, ParseError> {
10561        let tok = self.consume(TokenType::Synth)?;
10562        let name = self.consume(TokenType::Identifier)?.value;
10563        let mut node = SynthDefinition {
10564            name,
10565            loc: Loc {
10566                line: tok.line,
10567                column: tok.column,
10568            },
10569            ..Default::default()
10570        };
10571        self.consume(TokenType::LBrace)?;
10572        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10573            let key = self.consume_any_ident_or_kw()?.value;
10574            self.consume(TokenType::Colon)?;
10575            match key.as_str() {
10576                "target" => node.target = self.consume(TokenType::StringLit)?.value,
10577                "risk" => node.risk = self.consume_any_ident_or_kw()?.value,
10578                "language" => node.language = self.consume_any_ident_or_kw()?.value,
10579                "sandbox" => node.sandbox = self.consume_any_ident_or_kw()?.value,
10580                "review" => node.review = self.consume_any_ident_or_kw()?.value,
10581                "max_lines" => node.max_lines = self.parse_optional_int(),
10582                other => {
10583                    return Err(self.error(&format!(
10584                        "unknown synth field `{other}` in synth `{}` — expected `target` / `risk` \
10585                         / `language` / `sandbox` / `review` / `max_lines`",
10586                        node.name
10587                    )))
10588                }
10589            }
10590            if self.check(TokenType::Comma) {
10591                self.consume(TokenType::Comma)?;
10592            }
10593        }
10594        self.consume(TokenType::RBrace)?;
10595        Ok(node)
10596    }
10597
10598    /// v2.37.0 — parse `voice Name { fields }`. Cross-field laws
10599    /// (stt/tts XOR realtime, interruptible ⇒ legal_basis, ref resolution)
10600    /// are v2.37.0 type-checker territory (T852), same parse/check split as
10601    /// every primitive in this file.
10602    fn parse_voice(&mut self) -> Result<VoiceDefinition, ParseError> {
10603        let tok = self.consume(TokenType::Voice)?;
10604        let name = self.consume(TokenType::Identifier)?.value;
10605        let mut node = VoiceDefinition {
10606            name,
10607            loc: Loc { line: tok.line, column: tok.column },
10608            ..Default::default()
10609        };
10610        self.consume(TokenType::LBrace)?;
10611        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10612            let key = self.consume_any_ident_or_kw()?.value;
10613            self.consume(TokenType::Colon)?;
10614            match key.as_str() {
10615                // Each leg: a declared upstream name or a `Preset@vN` ref.
10616                "stt" => node.stt = Some(self.parse_upstream_ref()?),
10617                "tts" => node.tts = Some(self.parse_upstream_ref()?),
10618                "realtime" => node.realtime = Some(self.parse_upstream_ref()?),
10619                "carrier" => node.carrier = self.consume_any_ident_or_kw()?.value,
10620                "interruptible" => {
10621                    let v = self.consume_any_ident_or_kw()?.value;
10622                    node.interruptible = v == "true";
10623                }
10624                "legal_basis" => node.legal_basis = Some(self.consume_any_ident_or_kw()?.value),
10625                "persona" => node.persona = Some(self.consume(TokenType::Identifier)?.value),
10626                "context" => node.context = Some(self.consume(TokenType::Identifier)?.value),
10627                other => return Err(self.error(&format!("unknown voice field `{other}`"))),
10628            }
10629            if self.check(TokenType::Comma) {
10630                self.consume(TokenType::Comma)?;
10631            }
10632        }
10633        self.consume(TokenType::RBrace)?;
10634        Ok(node)
10635    }
10636
10637    /// v2.37.0 — an upstream leg reference: `Ident` (a declared
10638    /// `upstream`) or `Ident@vN` (a v2.37.0 preset).
10639    fn parse_upstream_ref(&mut self) -> Result<String, ParseError> {
10640        let base = self.consume(TokenType::Identifier)?.value;
10641        if self.check(TokenType::At) {
10642            self.advance();
10643            let version = self.consume_any_ident_or_kw()?.value;
10644            Ok(format!("{base}@{version}"))
10645        } else {
10646            Ok(base)
10647        }
10648    }
10649
10650    /// v2.37.0 — parse the `map: [ rule, … ]` projection list.
10651    ///
10652    /// rule := (`send` | `receive`) <MessageType> `as` (`json` | `binary`)
10653    ///         [ `tag` <string> ]                 — send-json only
10654    ///         [ `when` <string> `=` <string> ]   — receive-json only
10655    fn parse_upstream_map(&mut self) -> Result<Vec<UpstreamMapRule>, ParseError> {
10656        self.consume(TokenType::LBracket)?;
10657        let mut rules = Vec::new();
10658        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
10659            let dir_tok = self.current().clone();
10660            let direction = match dir_tok.ttype {
10661                TokenType::Send => "send",
10662                TokenType::Receive => "receive",
10663                _ => {
10664                    return Err(self.error(&format!(
10665                        "upstream map rule must start with `send` or `receive`, got `{}`",
10666                        dir_tok.value
10667                    )))
10668                }
10669            };
10670            self.advance();
10671            let message = self.consume(TokenType::Identifier)?.value;
10672            self.consume(TokenType::As)?;
10673            let framing = self.consume_any_ident_or_kw()?.value;
10674            let mut rule = UpstreamMapRule {
10675                direction: direction.to_string(),
10676                message,
10677                framing,
10678                loc: Loc { line: dir_tok.line, column: dir_tok.column },
10679                ..Default::default()
10680            };
10681            // Optional selectors — contextual identifiers, not keywords.
10682            if self.current().value == "tag" {
10683                self.advance();
10684                rule.tag = Some(self.consume(TokenType::StringLit)?.value);
10685            } else if self.current().value == "when" {
10686                // `when "f" = "v"` — equality discriminator; `when "f"` —
10687                // field-PRESENCE discriminator (vendors like Gemini Live /
10688                // ElevenLabs mark frame kinds by which key exists, not by a
10689                // type value).
10690                self.advance();
10691                rule.when_field = Some(self.consume(TokenType::StringLit)?.value);
10692                if self.check(TokenType::Assign) {
10693                    self.advance();
10694                    rule.when_value = Some(self.consume(TokenType::StringLit)?.value);
10695                }
10696            }
10697            rules.push(rule);
10698            if self.check(TokenType::Comma) {
10699                self.advance();
10700            }
10701        }
10702        self.consume(TokenType::RBracket)?;
10703        Ok(rules)
10704    }
10705
10706    /// v2.37.0 — parse `reconnect: { backoff_ms: <int>, max_attempts:
10707    /// <int>, on_exhausted: <ident> }` (order-free, all three required —
10708    /// a reconnection policy with a hole is not a policy).
10709    fn parse_upstream_reconnect(&mut self) -> Result<UpstreamReconnect, ParseError> {
10710        self.consume(TokenType::LBrace)?;
10711        let mut backoff_ms: Option<i64> = None;
10712        let mut max_attempts: Option<i64> = None;
10713        let mut on_exhausted: Option<String> = None;
10714        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10715            let key = self.consume_any_ident_or_kw()?.value;
10716            self.consume(TokenType::Colon)?;
10717            match key.as_str() {
10718                "backoff_ms" => {
10719                    backoff_ms = Some(
10720                        self.consume(TokenType::Integer)?
10721                            .value
10722                            .parse::<i64>()
10723                            .map_err(|_| self.error("backoff_ms must be an integer"))?,
10724                    )
10725                }
10726                "max_attempts" => {
10727                    max_attempts = Some(
10728                        self.consume(TokenType::Integer)?
10729                            .value
10730                            .parse::<i64>()
10731                            .map_err(|_| self.error("max_attempts must be an integer"))?,
10732                    )
10733                }
10734                "on_exhausted" => on_exhausted = Some(self.consume_any_ident_or_kw()?.value),
10735                other => return Err(self.error(&format!("unknown reconnect field `{other}`"))),
10736            }
10737            if self.check(TokenType::Comma) {
10738                self.consume(TokenType::Comma)?;
10739            }
10740        }
10741        self.consume(TokenType::RBrace)?;
10742        match (backoff_ms, max_attempts, on_exhausted) {
10743            (Some(b), Some(m), Some(o)) => Ok(UpstreamReconnect { backoff_ms: b, max_attempts: m, on_exhausted: o }),
10744            _ => Err(self.error(
10745                "reconnect requires all of `backoff_ms:`, `max_attempts:`, `on_exhausted:` — a reconnection policy with a hole is not a policy",
10746            )),
10747        }
10748    }
10749
10750    /// Parse: `[send T, receive U, loop, end]`.
10751    fn parse_session_steps(&mut self) -> Result<Vec<SessionStep>, ParseError> {
10752        self.consume(TokenType::LBracket)?;
10753        let mut steps = Vec::new();
10754        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
10755            steps.push(self.parse_session_step()?);
10756            if self.check(TokenType::Comma) {
10757                self.advance();
10758            }
10759        }
10760        self.consume(TokenType::RBracket)?;
10761        Ok(steps)
10762    }
10763
10764    /// v2.36.0 — a **brace**-delimited session step block: `{ step, step, … }`.
10765    /// Used by the `interrupt`/`resumable` regions (the paper's block surface),
10766    /// as opposed to the `[ … ]` step-lists used by roles and choice arms.
10767    fn parse_session_step_block(&mut self) -> Result<Vec<SessionStep>, ParseError> {
10768        self.consume(TokenType::LBrace)?;
10769        let mut steps = Vec::new();
10770        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10771            steps.push(self.parse_session_step()?);
10772            if self.check(TokenType::Comma) {
10773                self.advance();
10774            }
10775        }
10776        self.consume(TokenType::RBrace)?;
10777        Ok(steps)
10778    }
10779
10780    fn parse_session_step(&mut self) -> Result<SessionStep, ParseError> {
10781        let tok = self.current().clone();
10782        let loc = Loc { line: tok.line, column: tok.column };
10783        match tok.ttype {
10784            TokenType::Send => {
10785                self.advance();
10786                let msg = self.consume_any_ident_or_kw()?;
10787                Ok(SessionStep { op: "send".into(), message_type: msg.value, loc, ..Default::default() })
10788            }
10789            TokenType::Receive => {
10790                self.advance();
10791                let msg = self.consume_any_ident_or_kw()?;
10792                Ok(SessionStep { op: "receive".into(), message_type: msg.value, loc, ..Default::default() })
10793            }
10794            TokenType::Loop => {
10795                self.advance();
10796                Ok(SessionStep { op: "loop".into(), loc, ..Default::default() })
10797            }
10798            TokenType::End => {
10799                self.advance();
10800                Ok(SessionStep { op: "end".into(), loc, ..Default::default() })
10801            }
10802            // v2.3.0 — choice: `select { ℓ: [..], … }` (⊕) | `branch { ℓ: [..], … }` (&).
10803            // `select`/`branch` are not keywords — they arrive as identifiers.
10804            TokenType::Identifier if tok.value == "select" || tok.value == "branch" => {
10805                self.parse_session_choice(&tok.value, loc)
10806            }
10807            // v2.36.0 — `interrupt { <body> } on <Signal> as <sig> resumable { <handler> }`.
10808            // Contextual keyword (identifier), like `select`/`branch`.
10809            TokenType::Identifier if tok.value == "interrupt" => {
10810                self.parse_session_interrupt(loc)
10811            }
10812            // v2.36.0 — `resume`: the handler's normal exit (hand control back to
10813            // the parked body). A bare step, no payload; only meaningful inside an
10814            // `interrupt` handler (enforced at type-check, v2.36.0).
10815            //
10816            // ⚠️ v2.87.0 — this guard used to require `TokenType::Identifier`,
10817            // and `resume` became a HARD KEYWORD when the algebraic-effect
10818            // constructs landed. The session `resume` is a DIFFERENT `resume`
10819            // (v2.36.0's interrupt-handler exit, not v2.87.0's one-shot continuation
10820            // invocation), and it broke the moment the lexer stopped handing it
10821            // over as an identifier — `axon-frontend/src/voice_desugar.rs`'s own
10822            // expansion source stopped parsing.
10823            //
10824            // Matching on the VALUE rather than the token type is what keeps a
10825            // contextual keyword contextual. This was caught by the corpus gate
10826            // (`effect_grammar::a7_…`), not by review: six new hard
10827            // keywords across a 106-file `.axon` corpus is not a risk anyone
10828            // eyeballs correctly.
10829            _ if tok.value == "resume" => {
10830                self.advance();
10831                Ok(SessionStep { op: "resume".into(), loc, ..Default::default() })
10832            }
10833            _ => Err(ParseError {
10834                message: format!(
10835                    "Invalid session step '{}' — expected send | receive | loop | end | select | branch | interrupt | resume",
10836                    tok.value
10837                ),
10838                line: tok.line,
10839                column: tok.column,
10840                ..Default::default()
10841            }),
10842        }
10843    }
10844
10845    /// v2.36.0 — consume a **contextual keyword** (`on` / `as` / `resumable`):
10846    /// a token whose *value* must equal `kw`, regardless of whether the lexer
10847    /// classified it as a keyword or a bare identifier. Keeps the `interrupt`
10848    /// surface readable without minting three reserved words.
10849    fn consume_contextual(&mut self, kw: &str) -> Result<(), ParseError> {
10850        let t = self.current().clone();
10851        if t.value != kw {
10852            return Err(ParseError {
10853                message: format!("expected `{kw}` in interrupt step, got `{}`", t.value),
10854                line: t.line,
10855                column: t.column,
10856                ..Default::default()
10857            });
10858        }
10859        self.advance();
10860        Ok(())
10861    }
10862
10863    /// v2.36.0 — Parse an interruptible region:
10864    /// `interrupt { <body-steps> } on <Signal> as <sig> resumable { <handler-steps> }`.
10865    ///
10866    /// Encoded into the string-tagged `SessionStep` (mirroring the v2.3.0 choice
10867    /// shape): `op = "interrupt"`, `message_type = <Signal>` (validated against the
10868    /// closed `CallInterruptCause` catalog at type-check, v2.36.0), two labelled
10869    /// `branches` (`body`, `handler`), `binder = <sig>`, `resumable = true`.
10870    fn parse_session_interrupt(&mut self, loc: Loc) -> Result<SessionStep, ParseError> {
10871        self.advance(); // consume `interrupt`
10872        // Body region — a brace-delimited step block (the paper's `interrupt { … }`
10873        // surface; distinct from the `[ … ]` step-lists of roles/choice arms).
10874        let body = self.parse_session_step_block()?;
10875        // `on <Signal>`
10876        self.consume_contextual("on")?;
10877        let signal = self.consume_any_ident_or_kw()?;
10878        // `as <sig>`
10879        self.consume_contextual("as")?;
10880        let binder = self.consume_any_ident_or_kw()?;
10881        // `resumable { <handler> }`
10882        self.consume_contextual("resumable")?;
10883        let handler = self.parse_session_step_block()?;
10884        Ok(SessionStep {
10885            op: "interrupt".into(),
10886            message_type: signal.value,
10887            branches: vec![
10888                SessionBranch { label: "body".into(), steps: body, loc: loc.clone() },
10889                SessionBranch { label: "handler".into(), steps: handler, loc: loc.clone() },
10890            ],
10891            binder: binder.value,
10892            resumable: true,
10893            loc,
10894        })
10895    }
10896
10897    /// v2.3.0 — Parse a choice step: `select { ask: [..], cancel: [..] }`
10898    /// (or `branch { … }`). Each `label: [steps]` arm is a nested sub-protocol.
10899    fn parse_session_choice(&mut self, op: &str, loc: Loc) -> Result<SessionStep, ParseError> {
10900        self.advance(); // consume `select` / `branch`
10901        self.consume(TokenType::LBrace)?;
10902        let mut branches = Vec::new();
10903        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10904            let label_tok = self.consume_any_ident_or_kw()?;
10905            self.consume(TokenType::Colon)?;
10906            let steps = self.parse_session_steps()?;
10907            branches.push(SessionBranch {
10908                label: label_tok.value,
10909                steps,
10910                loc: Loc { line: label_tok.line, column: label_tok.column },
10911            });
10912            if self.check(TokenType::Comma) {
10913                self.advance();
10914            }
10915        }
10916        self.consume(TokenType::RBrace)?;
10917        Ok(SessionStep { op: op.to_string(), branches, loc, ..Default::default() })
10918    }
10919
10920    /// Parse: `topology Name { nodes: [A, B, …]  edges: [A -> B : Session, …] }`.
10921    fn parse_topology(&mut self) -> Result<TopologyDefinition, ParseError> {
10922        let tok = self.consume(TokenType::Topology)?;
10923        let name = self.consume(TokenType::Identifier)?.value;
10924        let mut node = TopologyDefinition {
10925            name,
10926            nodes: Vec::new(),
10927            edges: Vec::new(),
10928            loc: Loc {
10929                line: tok.line,
10930                column: tok.column,
10931            },
10932            leading_trivia: Vec::new(),
10933            trailing_trivia: Vec::new(),
10934        };
10935        self.consume(TokenType::LBrace)?;
10936        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10937            let field_name = self.current().value.clone();
10938            self.advance();
10939            if !self.check(TokenType::Colon) {
10940                if self.check(TokenType::LBrace) {
10941                    self.skip_braced_block()?;
10942                }
10943                continue;
10944            }
10945            self.advance();
10946            match field_name.as_str() {
10947                "nodes" => node.nodes = self.parse_bracketed_identifiers()?,
10948                "edges" => node.edges = self.parse_topology_edges()?,
10949                _ => self.skip_value(),
10950            }
10951        }
10952        self.consume(TokenType::RBrace)?;
10953        Ok(node)
10954    }
10955
10956    fn parse_topology_edges(&mut self) -> Result<Vec<TopologyEdge>, ParseError> {
10957        self.consume(TokenType::LBracket)?;
10958        let mut edges = Vec::new();
10959        while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
10960            edges.push(self.parse_topology_edge()?);
10961            if self.check(TokenType::Comma) {
10962                self.advance();
10963            }
10964        }
10965        self.consume(TokenType::RBracket)?;
10966        Ok(edges)
10967    }
10968
10969    fn parse_topology_edge(&mut self) -> Result<TopologyEdge, ParseError> {
10970        let src_tok = self.consume_any_ident_or_kw()?;
10971        self.consume(TokenType::Arrow)?;
10972        let tgt_tok = self.consume_any_ident_or_kw()?;
10973        self.consume(TokenType::Colon)?;
10974        let sess_tok = self.consume_any_ident_or_kw()?;
10975        Ok(TopologyEdge {
10976            source: src_tok.value,
10977            target: tgt_tok.value,
10978            session_ref: sess_tok.value,
10979            loc: Loc {
10980                line: src_tok.line,
10981                column: src_tok.column,
10982            },
10983        })
10984    }
10985
10986    // ── v1.1.0 — Cognitive immune system (paper_immune_v2.md) ────
10987
10988    /// Parse: `immune Name { watch, sensitivity, baseline, window, scope, tau, decay }`.
10989    fn parse_immune(&mut self) -> Result<ImmuneDefinition, ParseError> {
10990        let tok = self.consume(TokenType::Immune)?;
10991        let name = self.consume(TokenType::Identifier)?.value;
10992        let mut node = ImmuneDefinition {
10993            name,
10994            watch: Vec::new(),
10995            sensitivity: None,
10996            baseline: "learned".to_string(),
10997            window: 100,
10998            scope: String::new(),
10999            tau: String::new(),
11000            decay: "exponential".to_string(),
11001            loc: Loc {
11002                line: tok.line,
11003                column: tok.column,
11004            },
11005            leading_trivia: Vec::new(),
11006            trailing_trivia: Vec::new(),
11007        };
11008        self.consume(TokenType::LBrace)?;
11009        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11010            let field_name = self.current().value.clone();
11011            self.advance();
11012            if !self.check(TokenType::Colon) {
11013                if self.check(TokenType::LBrace) {
11014                    self.skip_braced_block()?;
11015                }
11016                continue;
11017            }
11018            self.advance();
11019            match field_name.as_str() {
11020                "watch" => node.watch = self.parse_bracketed_identifiers()?,
11021                "sensitivity" => node.sensitivity = self.parse_optional_float(),
11022                "baseline" => node.baseline = self.consume_any_ident_or_kw()?.value,
11023                "window" => {
11024                    if let Some(v) = self.parse_optional_int() {
11025                        node.window = v;
11026                    }
11027                }
11028                "scope" => {
11029                    let s_tok = self.consume_any_ident_or_kw()?;
11030                    let s = s_tok.value;
11031                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
11032                        return Err(ParseError {
11033                            message: format!(
11034                                "Invalid scope '{s}' in immune '{}' — \
11035                                 expected tenant | flow | global",
11036                                node.name
11037                            ),
11038                            line: s_tok.line,
11039                            column: s_tok.column,
11040                                                    ..Default::default()
11041                        });
11042                    }
11043                    node.scope = s;
11044                }
11045                "tau" => {
11046                    let t = self.current().clone();
11047                    match t.ttype {
11048                        TokenType::Duration | TokenType::StringLit => {
11049                            self.advance();
11050                            node.tau = t.value;
11051                        }
11052                        _ => node.tau = self.consume_any_ident_or_kw()?.value,
11053                    }
11054                }
11055                "decay" => {
11056                    let d_tok = self.consume_any_ident_or_kw()?;
11057                    let d = d_tok.value;
11058                    if !matches!(d.as_str(), "exponential" | "linear" | "none") {
11059                        return Err(ParseError {
11060                            message: format!(
11061                                "Invalid decay '{d}' in immune '{}' — \
11062                                 expected exponential | linear | none",
11063                                node.name
11064                            ),
11065                            line: d_tok.line,
11066                            column: d_tok.column,
11067                                                    ..Default::default()
11068                        });
11069                    }
11070                    node.decay = d;
11071                }
11072                _ => self.skip_value(),
11073            }
11074        }
11075        self.consume(TokenType::RBrace)?;
11076        Ok(node)
11077    }
11078
11079    /// Parse: `reflex Name { trigger, on_level, action, scope, sla }`.
11080    fn parse_reflex(&mut self) -> Result<ReflexDefinition, ParseError> {
11081        let tok = self.consume(TokenType::Reflex)?;
11082        let name = self.consume(TokenType::Identifier)?.value;
11083        let mut node = ReflexDefinition {
11084            name,
11085            trigger: String::new(),
11086            on_level: "doubt".to_string(),
11087            action: String::new(),
11088            scope: String::new(),
11089            sla: String::new(),
11090            loc: Loc {
11091                line: tok.line,
11092                column: tok.column,
11093            },
11094            leading_trivia: Vec::new(),
11095            trailing_trivia: Vec::new(),
11096        };
11097        self.consume(TokenType::LBrace)?;
11098        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11099            let field_name = self.current().value.clone();
11100            self.advance();
11101            if !self.check(TokenType::Colon) {
11102                if self.check(TokenType::LBrace) {
11103                    self.skip_braced_block()?;
11104                }
11105                continue;
11106            }
11107            self.advance();
11108            match field_name.as_str() {
11109                "trigger" => node.trigger = self.consume_any_ident_or_kw()?.value,
11110                "on_level" => {
11111                    let l_tok = self.consume_any_ident_or_kw()?;
11112                    let l = l_tok.value;
11113                    if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
11114                        return Err(ParseError {
11115                            message: format!(
11116                                "Invalid on_level '{l}' in reflex '{}' — \
11117                                 expected know | believe | speculate | doubt",
11118                                node.name
11119                            ),
11120                            line: l_tok.line,
11121                            column: l_tok.column,
11122                                                    ..Default::default()
11123                        });
11124                    }
11125                    node.on_level = l;
11126                }
11127                "action" => {
11128                    let a_tok = self.consume_any_ident_or_kw()?;
11129                    let a = a_tok.value;
11130                    if !matches!(
11131                        a.as_str(),
11132                        "drop"
11133                            | "revoke"
11134                            | "emit"
11135                            | "redact"
11136                            | "quarantine"
11137                            | "terminate"
11138                            | "alert"
11139                    ) {
11140                        return Err(ParseError {
11141                            message: format!(
11142                                "Invalid action '{a}' in reflex '{}' — \
11143                                 expected drop | revoke | emit | redact | \
11144                                 quarantine | terminate | alert",
11145                                node.name
11146                            ),
11147                            line: a_tok.line,
11148                            column: a_tok.column,
11149                                                    ..Default::default()
11150                        });
11151                    }
11152                    node.action = a;
11153                }
11154                "scope" => {
11155                    let s_tok = self.consume_any_ident_or_kw()?;
11156                    let s = s_tok.value;
11157                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
11158                        return Err(ParseError {
11159                            message: format!(
11160                                "Invalid scope '{s}' in reflex '{}' — \
11161                                 expected tenant | flow | global",
11162                                node.name
11163                            ),
11164                            line: s_tok.line,
11165                            column: s_tok.column,
11166                                                    ..Default::default()
11167                        });
11168                    }
11169                    node.scope = s;
11170                }
11171                "sla" => {
11172                    let t = self.current().clone();
11173                    match t.ttype {
11174                        TokenType::Duration | TokenType::StringLit => {
11175                            self.advance();
11176                            node.sla = t.value;
11177                        }
11178                        _ => node.sla = self.consume_any_ident_or_kw()?.value,
11179                    }
11180                }
11181                _ => self.skip_value(),
11182            }
11183        }
11184        self.consume(TokenType::RBrace)?;
11185        Ok(node)
11186    }
11187
11188    /// Parse: `heal Name { source, on_level, mode, scope, review_sla, shield, max_patches }`.
11189    fn parse_heal(&mut self) -> Result<HealDefinition, ParseError> {
11190        let tok = self.consume(TokenType::Heal)?;
11191        let name = self.consume(TokenType::Identifier)?.value;
11192        let mut node = HealDefinition {
11193            name,
11194            source: String::new(),
11195            on_level: "doubt".to_string(),
11196            mode: "human_in_loop".to_string(),
11197            scope: String::new(),
11198            review_sla: String::new(),
11199            shield_ref: String::new(),
11200            max_patches: 3,
11201            loc: Loc {
11202                line: tok.line,
11203                column: tok.column,
11204            },
11205            leading_trivia: Vec::new(),
11206            trailing_trivia: Vec::new(),
11207        };
11208        self.consume(TokenType::LBrace)?;
11209        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11210            let field_name = self.current().value.clone();
11211            self.advance();
11212            if !self.check(TokenType::Colon) {
11213                if self.check(TokenType::LBrace) {
11214                    self.skip_braced_block()?;
11215                }
11216                continue;
11217            }
11218            self.advance();
11219            match field_name.as_str() {
11220                "source" => node.source = self.consume_any_ident_or_kw()?.value,
11221                "on_level" => {
11222                    let l_tok = self.consume_any_ident_or_kw()?;
11223                    let l = l_tok.value;
11224                    if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
11225                        return Err(ParseError {
11226                            message: format!(
11227                                "Invalid on_level '{l}' in heal '{}' — \
11228                                 expected know | believe | speculate | doubt",
11229                                node.name
11230                            ),
11231                            line: l_tok.line,
11232                            column: l_tok.column,
11233                                                    ..Default::default()
11234                        });
11235                    }
11236                    node.on_level = l;
11237                }
11238                "mode" => {
11239                    let m_tok = self.consume_any_ident_or_kw()?;
11240                    let m = m_tok.value;
11241                    if !matches!(m.as_str(), "audit_only" | "human_in_loop" | "adversarial") {
11242                        return Err(ParseError {
11243                            message: format!(
11244                                "Invalid mode '{m}' in heal '{}' — \
11245                                 expected audit_only | human_in_loop | adversarial",
11246                                node.name
11247                            ),
11248                            line: m_tok.line,
11249                            column: m_tok.column,
11250                                                    ..Default::default()
11251                        });
11252                    }
11253                    node.mode = m;
11254                }
11255                "scope" => {
11256                    let s_tok = self.consume_any_ident_or_kw()?;
11257                    let s = s_tok.value;
11258                    if !matches!(s.as_str(), "tenant" | "flow" | "global") {
11259                        return Err(ParseError {
11260                            message: format!(
11261                                "Invalid scope '{s}' in heal '{}' — \
11262                                 expected tenant | flow | global",
11263                                node.name
11264                            ),
11265                            line: s_tok.line,
11266                            column: s_tok.column,
11267                                                    ..Default::default()
11268                        });
11269                    }
11270                    node.scope = s;
11271                }
11272                "review_sla" => {
11273                    let t = self.current().clone();
11274                    match t.ttype {
11275                        TokenType::Duration | TokenType::StringLit => {
11276                            self.advance();
11277                            node.review_sla = t.value;
11278                        }
11279                        _ => node.review_sla = self.consume_any_ident_or_kw()?.value,
11280                    }
11281                }
11282                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
11283                "max_patches" => {
11284                    if let Some(v) = self.parse_optional_int() {
11285                        node.max_patches = v;
11286                    }
11287                }
11288                _ => self.skip_value(),
11289            }
11290        }
11291        self.consume(TokenType::RBrace)?;
11292        Ok(node)
11293    }
11294
11295    // ── v1.3.1 — UI cognitiva (component / view) ────────────
11296
11297    /// Parse: `component Name { renders, via_shield, on_interact, render_hint }`.
11298    fn parse_component(&mut self) -> Result<ComponentDefinition, ParseError> {
11299        let tok = self.consume(TokenType::Component)?;
11300        let name = self.consume(TokenType::Identifier)?.value;
11301        let mut node = ComponentDefinition {
11302            name,
11303            renders: String::new(),
11304            via_shield: String::new(),
11305            on_interact: String::new(),
11306            render_hint: "custom".to_string(),
11307            loc: Loc {
11308                line: tok.line,
11309                column: tok.column,
11310            },
11311            leading_trivia: Vec::new(),
11312            trailing_trivia: Vec::new(),
11313        };
11314        self.consume(TokenType::LBrace)?;
11315        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11316            let field_name = self.current().value.clone();
11317            self.advance();
11318            if !self.check(TokenType::Colon) {
11319                if self.check(TokenType::LBrace) {
11320                    self.skip_braced_block()?;
11321                }
11322                continue;
11323            }
11324            self.advance();
11325            match field_name.as_str() {
11326                "renders" => node.renders = self.consume_any_ident_or_kw()?.value,
11327                "via_shield" => node.via_shield = self.consume_any_ident_or_kw()?.value,
11328                "on_interact" => node.on_interact = self.consume_any_ident_or_kw()?.value,
11329                "render_hint" => {
11330                    let h_tok = self.consume_any_ident_or_kw()?;
11331                    let h = h_tok.value;
11332                    if !matches!(h.as_str(), "card" | "list" | "form" | "chart" | "custom") {
11333                        return Err(ParseError {
11334                            message: format!(
11335                                "Invalid render_hint '{h}' in component '{}' — \
11336                                 expected card | list | form | chart | custom",
11337                                node.name
11338                            ),
11339                            line: h_tok.line,
11340                            column: h_tok.column,
11341                                                    ..Default::default()
11342                        });
11343                    }
11344                    node.render_hint = h;
11345                }
11346                _ => self.skip_value(),
11347            }
11348        }
11349        self.consume(TokenType::RBrace)?;
11350        Ok(node)
11351    }
11352
11353    /// Parse: `view Name { title, components: [...], route }`.
11354    fn parse_view(&mut self) -> Result<ViewDefinition, ParseError> {
11355        let tok = self.consume(TokenType::View)?;
11356        let name = self.consume(TokenType::Identifier)?.value;
11357        let mut node = ViewDefinition {
11358            name,
11359            title: String::new(),
11360            components: Vec::new(),
11361            route: String::new(),
11362            loc: Loc {
11363                line: tok.line,
11364                column: tok.column,
11365            },
11366            leading_trivia: Vec::new(),
11367            trailing_trivia: Vec::new(),
11368        };
11369        self.consume(TokenType::LBrace)?;
11370        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11371            let field_name = self.current().value.clone();
11372            self.advance();
11373            if !self.check(TokenType::Colon) {
11374                if self.check(TokenType::LBrace) {
11375                    self.skip_braced_block()?;
11376                }
11377                continue;
11378            }
11379            self.advance();
11380            match field_name.as_str() {
11381                "title" => node.title = self.consume(TokenType::StringLit)?.value,
11382                "components" => node.components = self.parse_bracketed_identifiers()?,
11383                "route" => node.route = self.consume(TokenType::StringLit)?.value,
11384                _ => self.skip_value(),
11385            }
11386        }
11387        self.consume(TokenType::RBrace)?;
11388        Ok(node)
11389    }
11390
11391    fn parse_axonendpoint(&mut self) -> Result<AxonEndpointDefinition, ParseError> {
11392        let tok = self.consume(TokenType::AxonEndpoint)?;
11393        let name = self.consume(TokenType::Identifier)?.value;
11394        let mut node = AxonEndpointDefinition {
11395            name,
11396            method: String::new(),
11397            path: String::new(),
11398            body_type: String::new(),
11399            execute_flow: String::new(),
11400            output_type: String::new(),
11401            shield_ref: String::new(),
11402            // v2.38.0 — `cors:` reference; empty ≡ no cors declared
11403            // (the design decision: no CORS headers, ever — secure by default).
11404            cors_ref: String::new(),
11405            retries: None,
11406            timeout: String::new(),
11407            compliance: Vec::new(),
11408            // v1.21.0 — Defaults preserve backwards compat per D1.
11409            transport: "json".to_string(),
11410            keepalive: String::new(),
11411            // v1.22.0 — Inference fields (parser-default state).
11412            // Both fields toggle/populate only when the source provides
11413            // an explicit `transport:` declaration (parser sets
11414            // `transport_explicit = true`) AND the type-checker walks
11415            // the program to compute `implicit_transport`.
11416            transport_explicit: false,
11417            implicit_transport: String::new(),
11418            // v1.23.0 (D8) — auth scope; empty list ≡ no auth gate.
11419            requires_capabilities: Vec::new(),
11420            // v2.44.0 — explicit authorization-coverage opt-out. Default
11421            // false; the v2.44.0 rule requires coverage OR `public: true`.
11422            public: false,
11423            // v1.23.0 — Replay-token binding (D9 plan-vivo).
11424            // Parser defaults: not explicit; effective value resolved
11425            // at deploy time using the method-default heuristic.
11426            replay_explicit: false,
11427            replay: false,
11428            // v1.28.0 — Wire-format dialect default
11429            // empty; the runtime classifier resolves the default
11430            // dialect per the algebraic-effect predicate when the
11431            // source omits `transport: sse(<dialect>)`.
11432            transport_dialect: String::new(),
11433            // v1.27.1 — Algebraic-effect override.
11434            // Parser default false; populated by the type-checker's
11435            // compute_implicit_transports pass once the full program
11436            // is known (the predicate cross-references tool effects
11437            // declared anywhere in the program).
11438            has_algebraic_stream_effect: false,
11439            // v1.31.0 (D2) — declared execution backend; empty ≡
11440            // not declared (the endpoint resolves down the v1.31.0 D1
11441            // ladder). A non-empty value is validated against the
11442            // closed `AXONENDPOINT_BACKEND_VALUES` catalog below.
11443            backend: String::new(),
11444            // v1.32.0 (D1) — Path-param names extracted from the
11445            // `path:` string AFTER the field is parsed. Initialized
11446            // empty; populated by `extract_path_param_names` after
11447            // the `path:` field is read in the loop below.
11448            path_params: Vec::new(),
11449            // v1.32.0 (D2) — Inline `query: { name: Type, name: Type? }`
11450            // block. Initialized empty; populated by the `"query"` arm
11451            // in the field loop below. Closed catalog enforced at parse
11452            // time per `axonendpoint_is_valid_query_param_type`.
11453            query_params: Vec::new(),
11454            loc: Loc {
11455                line: tok.line,
11456                column: tok.column,
11457            },
11458            leading_trivia: Vec::new(),
11459            trailing_trivia: Vec::new(),
11460        };
11461        self.consume(TokenType::LBrace)?;
11462        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11463            let field_name = self.current().value.clone();
11464            self.advance();
11465            if self.check(TokenType::Colon) {
11466                self.advance();
11467                match field_name.as_str() {
11468                    "method" => {
11469                        // v1.23.0 D3 — closed method enum
11470                        // `{GET, POST, PUT, DELETE, PATCH}`. Unknown
11471                        // values rejected at parse time with smart-
11472                        // suggest hint (v1.20.0). HEAD/OPTIONS/etc.
11473                        // are runtime-managed and not adopter-
11474                        // declarable.
11475                        let value_tok = self.consume_any_ident_or_kw()?;
11476                        let value_upper = value_tok.value.to_uppercase();
11477                        if !axonendpoint_is_valid_method(&value_upper) {
11478                            let hint = crate::smart_suggest::suggest_for(
11479                                &value_upper,
11480                                AXONENDPOINT_METHOD_VALUES,
11481                            );
11482                            let base = format!(
11483                                "Invalid method '{}' in axonendpoint '{}'.",
11484                                value_tok.value, node.name
11485                            );
11486                            let message = if hint.is_empty() {
11487                                format!(
11488                                    "{base} expected GET | POST | PUT | DELETE | PATCH, found {}",
11489                                    value_tok.value
11490                                )
11491                            } else {
11492                                format!(
11493                                    "{base} {hint} (expected GET | POST | PUT | DELETE | PATCH, found {})",
11494                                    value_tok.value
11495                                )
11496                            };
11497                            return Err(ParseError {
11498                                message,
11499                                line: value_tok.line,
11500                                column: value_tok.column,
11501                                ..Default::default()
11502                            });
11503                        }
11504                        node.method = value_upper;
11505                    }
11506                    "path" => {
11507                        node.path = self.consume(TokenType::StringLit)?.value.clone();
11508                        // v1.32.0 (D1) — extract `{name}` placeholders
11509                        // for the Request Binding Contract's path-param
11510                        // source. Duplicate `{name}` in the same path
11511                        // is rejected at parse time (HTTP route patterns
11512                        // structurally reject duplicates; surfacing the
11513                        // error here is friendlier than letting axum
11514                        // panic at registration).
11515                        match extract_path_param_names(&node.path) {
11516                            Ok(names) => node.path_params = names,
11517                            Err(dup) => {
11518                                let cur = self.current().clone();
11519                                return Err(ParseError {
11520                                    message: format!(
11521                                        "axonendpoint '{}' declares path '{}' \
11522                                         containing duplicate placeholder '{{{}}}'. \
11523                                         Each `{{name}}` in a `path:` must be \
11524                                         unique — the runtime cannot bind two \
11525                                         path segments to the same name.",
11526                                        node.name, node.path, dup,
11527                                    ),
11528                                    line: cur.line,
11529                                    column: cur.column,
11530                                    ..Default::default()
11531                                });
11532                            }
11533                        }
11534                    },
11535                    "body" => node.body_type = self.consume_any_ident_or_kw()?.value.clone(),
11536                    "query" => {
11537                        // v1.32.0 (D2) — Inline query-parameter block.
11538                        // Grammar: `query: { name: Type [, name: Type?]* }`.
11539                        // Closed type catalog
11540                        // `AXONENDPOINT_QUERY_PARAM_TYPES = {Text, Int,
11541                        // Float, Bool, Uuid}`. Optional via `?` suffix
11542                        // reuses `TypeExpr.optional` semantics already in
11543                        // use for flow parameters + body type fields. A
11544                        // duplicate field name in the same block is a
11545                        // parse error (HTTP query strings DO allow
11546                        // multi-value but v1.38.5 binds the first value
11547                        // only — see plan vivo section 7 forward-compat).
11548                        //
11549                        // v1.32.0 (D2 robustness) — declaring `query:`
11550                        // twice on the same axonendpoint silently merged
11551                        // params pre-hardening. Now it's a parse error
11552                        // so an adopter typo / copy-paste mistake
11553                        // surfaces with line + column instead of
11554                        // producing an unexpectedly-augmented endpoint.
11555                        let lbrace_tok = self.consume(TokenType::LBrace)?;
11556                        let block_line = lbrace_tok.line;
11557                        if !node.query_params.is_empty() {
11558                            return Err(ParseError {
11559                                message: format!(
11560                                    "axonendpoint '{}' declares `query: {{ … }}` \
11561                                     more than once. The query-parameter block \
11562                                     is unique per endpoint; combine all params \
11563                                     into a single block.",
11564                                    node.name,
11565                                ),
11566                                line: lbrace_tok.line,
11567                                column: lbrace_tok.column,
11568                                ..Default::default()
11569                            });
11570                        }
11571                        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11572                            let name_tok = self.consume(TokenType::Identifier)?;
11573                            let field_name = name_tok.value.clone();
11574                            // Duplicate detection within the block.
11575                            if node
11576                                .query_params
11577                                .iter()
11578                                .any(|f| f.name == field_name)
11579                            {
11580                                return Err(ParseError {
11581                                    message: format!(
11582                                        "axonendpoint '{}' declares duplicate \
11583                                         query param '{}' inside `query: {{ … }}`. \
11584                                         Each name must appear at most once \
11585                                         .",
11586                                        node.name, field_name,
11587                                    ),
11588                                    line: name_tok.line,
11589                                    column: name_tok.column,
11590                                    ..Default::default()
11591                                });
11592                            }
11593                            self.consume(TokenType::Colon)?;
11594                            let type_expr = self.parse_type_expr()?;
11595                            // v1.32.0 (D2 robustness) — reject generic
11596                            // type expressions on query params. The
11597                            // closed catalog is 5 primitives; container
11598                            // types (`Optional<T>`, `List<T>`, etc.)
11599                            // would mislead the adopter into thinking
11600                            // they bind multi-value query strings
11601                            // (deferred per plan vivo section 7) or that
11602                            // `Optional<Text>` is the canonical way to
11603                            // declare an optional query (it's NOT —
11604                            // `Text?` is). Surface the canonical syntax
11605                            // verbatim so the fix is obvious.
11606                            if !type_expr.generic_param.is_empty() {
11607                                let canonical_hint = if type_expr.name == "Optional" {
11608                                    format!(
11609                                        " Use `{}?` (the `?` suffix) for an \
11610                                         optional query param instead of \
11611                                         `Optional<{}>`.",
11612                                        type_expr.generic_param,
11613                                        type_expr.generic_param,
11614                                    )
11615                                } else if type_expr.name == "List" {
11616                                    " Multi-value query params (e.g. `?tag=a&tag=b`) \
11617                                     are honest-deferred from v1.38.5; bind a \
11618                                     single-value `Text` query param and parse \
11619                                     the value inside the flow."
11620                                        .to_string()
11621                                } else {
11622                                    String::new()
11623                                };
11624                                return Err(ParseError {
11625                                    message: format!(
11626                                        "axonendpoint '{}' query param '{}' uses \
11627                                         a generic type `{}<{}>`. Query params \
11628                                         take a primitive type from the closed \
11629                                         catalog ({}); the `?` suffix marks \
11630                                         optional.{} .",
11631                                        node.name,
11632                                        field_name,
11633                                        type_expr.name,
11634                                        type_expr.generic_param,
11635                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | "),
11636                                        canonical_hint,
11637                                    ),
11638                                    line: type_expr.loc.line,
11639                                    column: type_expr.loc.column,
11640                                    ..Default::default()
11641                                });
11642                            }
11643                            // Validate against the closed catalog. A
11644                            // miss surfaces a v1.20.0-style smart-suggest
11645                            // hint when within edit-distance 2.
11646                            if !axonendpoint_is_valid_query_param_type(&type_expr.name) {
11647                                // `smart_suggest::suggest_for` returns
11648                                // pre-formatted prose like
11649                                // "Did you mean `Text`?" or
11650                                // "Did you mean `Text` or `Int`?" (empty
11651                                // when no candidate within edit-distance
11652                                // 2). Concatenate without re-wrapping.
11653                                let hint = crate::smart_suggest::suggest_for(
11654                                    &type_expr.name,
11655                                    AXONENDPOINT_QUERY_PARAM_TYPES,
11656                                );
11657                                let hint_text = if hint.is_empty() {
11658                                    format!(
11659                                        " Expected one of: {}.",
11660                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | ")
11661                                    )
11662                                } else {
11663                                    format!(
11664                                        " {} Expected one of: {}.",
11665                                        hint,
11666                                        AXONENDPOINT_QUERY_PARAM_TYPES.join(" | ")
11667                                    )
11668                                };
11669                                return Err(ParseError {
11670                                    message: format!(
11671                                        "axonendpoint '{}' query param '{}' has \
11672                                         unsupported type '{}'.{} .",
11673                                        node.name, field_name, type_expr.name,
11674                                        hint_text,
11675                                    ),
11676                                    line: type_expr.loc.line,
11677                                    column: type_expr.loc.column,
11678                                    ..Default::default()
11679                                });
11680                            }
11681                            node.query_params.push(TypeField {
11682                                name: field_name,
11683                                type_expr,
11684                                loc: Loc {
11685                                    line: name_tok.line,
11686                                    column: name_tok.column,
11687                                },
11688                            });
11689                            // Trailing comma is optional; the next loop
11690                            // iteration handles `}` cleanly. Accept both
11691                            // `name: Type, name: Type` AND `name: Type
11692                            // name: Type` (the existing parser style is
11693                            // forgiving about list separators).
11694                            if self.check(TokenType::Comma) {
11695                                self.advance();
11696                            }
11697                            let _ = block_line; // suppress unused warning
11698                        }
11699                        self.consume(TokenType::RBrace)?;
11700                    },
11701                    "execute" => node.execute_flow = self.consume_any_ident_or_kw()?.value.clone(),
11702                    "output" => {
11703                        // v1.31.0 — promote axonendpoint `output:`
11704                        // parsing from a single token to the full
11705                        // generic-aware type expression (mirroring
11706                        // `parse_step` for FlowStep::Step which already
11707                        // uses `parse_output_type_string`).
11708                        //
11709                        // Pre-38.x.f: `output: List<Item>` captured only
11710                        // `"List"`, dropping `<Item>` (next tokens were
11711                        // either left unconsumed or absorbed by the
11712                        // following field). v1.39.0's narrow cardinality
11713                        // gate happened to fire correctly for `output: T`
11714                        // + retrieve-tail because the singular-detection
11715                        // path used `!starts_with("List<")` — but the
11716                        // SYMMETRIC `output: List<T>` + singular-tail
11717                        // case (38.x.f D3) needs the FULL `List<T>`
11718                        // shape captured; without it the gate sees
11719                        // `"List"` and misclassifies as Singular.
11720                        node.output_type = self.parse_output_type_string()?;
11721                    }
11722                    "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
11723                    // v2.38.0 — the `cors: <Name>` reference.
11724                    "cors" => node.cors_ref = self.consume_any_ident_or_kw()?.value.clone(),
11725                    "retries" => node.retries = self.parse_optional_int(),
11726                    "timeout" => {
11727                        let t = self.current().clone();
11728                        self.advance();
11729                        node.timeout = t.value.clone();
11730                    }
11731                    "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
11732                    "replay" => {
11733                        // v1.23.0 (D9 plan-vivo) — Replay-token binding.
11734                        // Boolean `replay: true | false`. Default (when
11735                        // omitted) is method-derived at deploy-time:
11736                        // POST/PUT → true, GET/DELETE → false. Explicit
11737                        // declaration sets `replay_explicit = true` so
11738                        // the runtime knows NOT to override.
11739                        let value_tok = self.consume(TokenType::Bool)?;
11740                        node.replay = value_tok.value.eq_ignore_ascii_case("true");
11741                        node.replay_explicit = true;
11742                    }
11743                    // v2.44.0 — `public: true | false`, the explicit
11744                    // authorization-coverage opt-out (doctrine
11745                    // `every_boundary_is_guarded`). Mirrors `replay:`'s bool
11746                    // parse. Default false; the v2.44.0 rule (`axon-T890`)
11747                    // requires a covering discipline OR `public: true`.
11748                    "public" => {
11749                        let value_tok = self.consume(TokenType::Bool)?;
11750                        node.public = value_tok.value.eq_ignore_ascii_case("true");
11751                    }
11752                    "requires" => {
11753                        // v1.23.0 (D8) — Auth scope per axonendpoint.
11754                        // Closed slug grammar
11755                        // `^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$` enforced
11756                        // at parse time with smart-suggest-style hint.
11757                        // Empty list means "no auth gate" (D9 backwards-
11758                        // compat). Cross-stack with Python parser.
11759                        let bracket_tok = self.current().clone();
11760                        let items = self.parse_bracketed_dot_identifiers()?;
11761                        for slug in &items {
11762                            if !is_valid_capability_slug(slug) {
11763                                return Err(ParseError {
11764                                    message: format!(
11765                                        "Invalid capability slug '{slug}' in axonendpoint '{}' \
11766                                         `requires:`. Capability slugs must match \
11767                                         ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
11768                                         lowercase identifiers starting with a letter. Examples: \
11769                                         `admin`, `legal.read`, `hipaa.phi.read`.",
11770                                        node.name
11771                                    ),
11772                                    line: bracket_tok.line,
11773                                    column: bracket_tok.column,
11774                                    ..Default::default()
11775                                });
11776                            }
11777                        }
11778                        node.requires_capabilities = items;
11779                    }
11780                    // v1.21.0 — HTTP transport enum (D2 closed) + keepalive (D6 closed).
11781                    // Mirrors `axon/compiler/parser.py` `_parse_axonendpoint`.
11782                    // Drift-gate corpus verifies byte-identical parse cross-stack.
11783                    "transport" => {
11784                        let value_tok = self.consume_any_ident_or_kw()?;
11785                        let value = &value_tok.value;
11786                        if !axonendpoint_is_valid_transport(value) {
11787                            let hint = crate::smart_suggest::suggest_for(
11788                                value,
11789                                AXONENDPOINT_TRANSPORT_VALUES,
11790                            );
11791                            let base = format!(
11792                                "Invalid transport '{}' in axonendpoint '{}'.",
11793                                value, node.name
11794                            );
11795                            let message = if hint.is_empty() {
11796                                format!("{base} expected json | sse | ndjson, found {value}")
11797                            } else {
11798                                format!(
11799                                    "{base} {hint} (expected json | sse | ndjson, found {value})"
11800                                )
11801                            };
11802                            return Err(ParseError {
11803                                message,
11804                                line: value_tok.line,
11805                                column: value_tok.column,
11806                                ..Default::default()
11807                            });
11808                        }
11809                        node.transport = value.clone();
11810                        // v1.22.0 D1 — mark the field as explicitly
11811                        // declared so the type-checker's implicit-transport
11812                        // inference knows NOT to override this value with
11813                        // the produces_stream-driven inference.
11814                        node.transport_explicit = true;
11815                        // v1.28.0 — Optional dialect
11816                        // parametrization: `transport: sse(<dialect>)`.
11817                        // Only valid when the base value is `sse`
11818                        // (json + ndjson dialects are the dialects
11819                        // themselves; `json(<x>)` / `ndjson(<x>)`
11820                        // would be parse errors caught below).
11821                        if self.check(TokenType::LParen) {
11822                            if value != "sse" {
11823                                let tok = self.current().clone();
11824                                return Err(ParseError {
11825                                    message: format!(
11826                                        "Dialect parametrization \
11827                                         `transport: {value}(<dialect>)` is \
11828                                         only valid for `sse`; got \
11829                                         `{value}` in axonendpoint '{}'.",
11830                                        node.name
11831                                    ),
11832                                    line: tok.line,
11833                                    column: tok.column,
11834                                    ..Default::default()
11835                                });
11836                            }
11837                            self.advance(); // consume LParen
11838                            let dialect_tok = self.consume_any_ident_or_kw()?;
11839                            let dialect = dialect_tok.value.clone();
11840                            if !AXONENDPOINT_TRANSPORT_DIALECTS
11841                                .iter()
11842                                .any(|&d| d == dialect)
11843                            {
11844                                let hint = crate::smart_suggest::suggest_for(
11845                                    &dialect,
11846                                    AXONENDPOINT_TRANSPORT_DIALECTS,
11847                                );
11848                                let base = format!(
11849                                    "Invalid SSE dialect '{dialect}' in axonendpoint '{}'.",
11850                                    node.name
11851                                );
11852                                let message = if hint.is_empty() {
11853                                    format!(
11854                                        "{base} expected axon | openai | kimi | glm | anthropic, found {dialect}"
11855                                    )
11856                                } else {
11857                                    format!(
11858                                        "{base} {hint} (expected axon | openai | kimi | glm | anthropic, found {dialect})"
11859                                    )
11860                                };
11861                                return Err(ParseError {
11862                                    message,
11863                                    line: dialect_tok.line,
11864                                    column: dialect_tok.column,
11865                                    ..Default::default()
11866                                });
11867                            }
11868                            // Closing RParen.
11869                            let rparen_tok = self.current().clone();
11870                            if !self.check(TokenType::RParen) {
11871                                return Err(ParseError {
11872                                    message: format!(
11873                                        "Expected `)` after dialect name \
11874                                         in axonendpoint '{}' \
11875                                         (transport: sse(<dialect>) grammar).",
11876                                        node.name
11877                                    ),
11878                                    line: rparen_tok.line,
11879                                    column: rparen_tok.column,
11880                                    ..Default::default()
11881                                });
11882                            }
11883                            self.advance(); // consume RParen
11884                            node.transport_dialect = dialect;
11885                        }
11886                    }
11887                    "keepalive" => {
11888                        // Accepts either a DURATION token (e.g. `15s`) or
11889                        // an ident-like token. Validation against the
11890                        // closed enum {5s, 15s, 30s, 60s} happens after.
11891                        let value_tok = self.current().clone();
11892                        self.advance();
11893                        let value = &value_tok.value;
11894                        if !axonendpoint_is_valid_keepalive(value) {
11895                            let hint = crate::smart_suggest::suggest_for(
11896                                value,
11897                                AXONENDPOINT_KEEPALIVE_VALUES,
11898                            );
11899                            let base = format!(
11900                                "Invalid keepalive '{}' in axonendpoint '{}'.",
11901                                value, node.name
11902                            );
11903                            let message = if hint.is_empty() {
11904                                format!("{base} expected 5s | 15s | 30s | 60s, found {value}")
11905                            } else {
11906                                format!(
11907                                    "{base} {hint} (expected 5s | 15s | 30s | 60s, found {value})"
11908                                )
11909                            };
11910                            return Err(ParseError {
11911                                message,
11912                                line: value_tok.line,
11913                                column: value_tok.column,
11914                                ..Default::default()
11915                            });
11916                        }
11917                        node.keepalive = value.clone();
11918                    }
11919                    "backend" => {
11920                        // v1.31.0 (D2) — declared execution backend.
11921                        // Closed catalog `CANONICAL_PROVIDERS ∪ {auto,
11922                        // stub}`; an unknown name is a parse error with
11923                        // a smart-suggest hint (the same discipline as
11924                        // `method`/`transport`/`keepalive`). The
11925                        // type-checker re-validates defensively for
11926                        // ASTs built outside the parser (LSP, tests).
11927                        let value_tok = self.consume_any_ident_or_kw()?;
11928                        let value = &value_tok.value;
11929                        if !axonendpoint_is_valid_backend(value) {
11930                            let hint = crate::smart_suggest::suggest_for(
11931                                value,
11932                                AXONENDPOINT_BACKEND_VALUES,
11933                            );
11934                            let expected = AXONENDPOINT_BACKEND_VALUES.join(" | ");
11935                            let base = format!(
11936                                "Invalid backend '{}' in axonendpoint '{}'.",
11937                                value, node.name
11938                            );
11939                            let message = if hint.is_empty() {
11940                                format!("{base} expected {expected}, found {value}")
11941                            } else {
11942                                format!(
11943                                    "{base} {hint} (expected {expected}, found {value})"
11944                                )
11945                            };
11946                            return Err(ParseError {
11947                                message,
11948                                line: value_tok.line,
11949                                column: value_tok.column,
11950                                ..Default::default()
11951                            });
11952                        }
11953                        node.backend = value.clone();
11954                    }
11955                    _ => self.skip_value(),
11956                }
11957            } else if self.check(TokenType::LBrace) {
11958                self.skip_braced_block()?;
11959            }
11960        }
11961        self.consume(TokenType::RBrace)?;
11962        Ok(node)
11963    }
11964
11965    // ── Numeric helpers for Tier 2 field parsing ────────────────────
11966
11967    fn parse_optional_int(&mut self) -> Option<i64> {
11968        let tok = self.current().clone();
11969        match tok.ttype {
11970            TokenType::Integer => {
11971                self.advance();
11972                tok.value.parse::<i64>().ok()
11973            }
11974            _ => {
11975                self.advance();
11976                None
11977            }
11978        }
11979    }
11980
11981    fn parse_optional_float(&mut self) -> Option<f64> {
11982        let tok = self.current().clone();
11983        match tok.ttype {
11984            TokenType::Float | TokenType::Integer => {
11985                self.advance();
11986                tok.value.parse::<f64>().ok()
11987            }
11988            _ => {
11989                self.advance();
11990                None
11991            }
11992        }
11993    }
11994
11995    // ── LAMBDA DATA (ΛD) ──────────────────────────────────────────
11996
11997    fn parse_lambda_data(&mut self) -> Result<LambdaDataDefinition, ParseError> {
11998        let tok = self.consume(TokenType::Lambda)?;
11999        let name = self.consume(TokenType::Identifier)?;
12000        self.consume(TokenType::LBrace)?;
12001
12002        let mut node = LambdaDataDefinition {
12003            name: name.value.clone(),
12004            ontology: String::new(),
12005            certainty: 1.0,
12006            temporal_frame_start: String::new(),
12007            temporal_frame_end: String::new(),
12008            provenance: String::new(),
12009            derivation: String::new(),
12010            loc: Loc {
12011                line: tok.line,
12012                column: tok.column,
12013            },
12014            leading_trivia: Vec::new(),
12015            trailing_trivia: Vec::new(),
12016        };
12017
12018        while !self.check(TokenType::RBrace) {
12019            let field = self.current().clone();
12020            match field.ttype {
12021                TokenType::Ontology => {
12022                    self.advance();
12023                    self.consume(TokenType::Colon)?;
12024                    node.ontology = self.consume(TokenType::StringLit)?.value.clone();
12025                }
12026                TokenType::Certainty => {
12027                    self.advance();
12028                    self.consume(TokenType::Colon)?;
12029                    let val = self.current().clone();
12030                    match val.ttype {
12031                        TokenType::Float => {
12032                            self.advance();
12033                            node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
12034                        }
12035                        TokenType::Integer => {
12036                            self.advance();
12037                            node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
12038                        }
12039                        _ => {
12040                            return Err(ParseError {
12041                                message: format!(
12042                                    "Expected number for certainty, got '{}'",
12043                                    val.value
12044                                ),
12045                                line: val.line,
12046                                column: val.column,
12047                                                            ..Default::default()
12048                            });
12049                        }
12050                    }
12051                }
12052                TokenType::TemporalFrame => {
12053                    self.advance();
12054                    self.consume(TokenType::Colon)?;
12055                    node.temporal_frame_start = self.consume(TokenType::StringLit)?.value.clone();
12056                    // Optional second string for end frame
12057                    if self.check(TokenType::StringLit) {
12058                        node.temporal_frame_end = self.consume(TokenType::StringLit)?.value.clone();
12059                    }
12060                }
12061                TokenType::Provenance => {
12062                    self.advance();
12063                    self.consume(TokenType::Colon)?;
12064                    node.provenance = self.consume(TokenType::StringLit)?.value.clone();
12065                }
12066                TokenType::Derivation => {
12067                    self.advance();
12068                    self.consume(TokenType::Colon)?;
12069                    let d = self.current().clone();
12070                    self.advance();
12071                    node.derivation = d.value.clone();
12072                }
12073                _ => {
12074                    // Skip unknown fields gracefully
12075                    self.advance();
12076                    if self.check(TokenType::Colon) {
12077                        self.advance();
12078                        self.skip_value();
12079                    }
12080                }
12081            }
12082        }
12083
12084        self.consume(TokenType::RBrace)?;
12085        Ok(node)
12086    }
12087
12088    fn parse_lambda_data_apply(&mut self) -> Result<LambdaDataApplyNode, ParseError> {
12089        let tok = self.consume(TokenType::Lambda)?;
12090        let lambda_name = self.consume(TokenType::Identifier)?;
12091
12092        // Expect "on" keyword (parsed as identifier since it's not reserved)
12093        let on_tok = self.current().clone();
12094        self.advance();
12095        if on_tok.value != "on" {
12096            return Err(ParseError {
12097                message: format!(
12098                    "Expected 'on' after lambda data name in flow step, got '{}'",
12099                    on_tok.value
12100                ),
12101                line: on_tok.line,
12102                column: on_tok.column,
12103                            ..Default::default()
12104            });
12105        }
12106
12107        let target = self.current().clone();
12108        self.advance();
12109
12110        let mut output_type = String::new();
12111        if self.check(TokenType::Arrow) {
12112            self.advance();
12113            output_type = self.consume(TokenType::Identifier)?.value.clone();
12114        }
12115
12116        Ok(LambdaDataApplyNode {
12117            lambda_data_name: lambda_name.value.clone(),
12118            target: target.value.clone(),
12119            output_type,
12120            loc: Loc {
12121                line: tok.line,
12122                column: tok.column,
12123            },
12124        })
12125    }
12126
12127    // ── GENERIC (Tier 2+) ────────────────────────────────────────
12128
12129    fn parse_generic_declaration(&mut self) -> Result<Declaration, ParseError> {
12130        let kw_tok = self.current().clone();
12131        self.advance(); // consume keyword
12132
12133        // Try to consume a name (identifier or keyword-as-name)
12134        let name = if self.current().ttype == TokenType::Identifier {
12135            let n = self.current().value.clone();
12136            self.advance();
12137            n
12138        } else if !self.check(TokenType::LBrace)
12139            && !self.check(TokenType::LParen)
12140            && !self.check(TokenType::Eof)
12141            && self
12142                .current()
12143                .value
12144                .chars()
12145                .all(|c| c.is_alphanumeric() || c == '_')
12146        {
12147            let n = self.current().value.clone();
12148            self.advance();
12149            n
12150        } else {
12151            String::new()
12152        };
12153
12154        // Skip optional parens: (...)
12155        if self.check(TokenType::LParen) {
12156            self.advance();
12157            let mut depth = 1u32;
12158            while depth > 0 && !self.check(TokenType::Eof) {
12159                if self.check(TokenType::LParen) {
12160                    depth += 1;
12161                } else if self.check(TokenType::RParen) {
12162                    depth -= 1;
12163                }
12164                self.advance();
12165            }
12166        }
12167
12168        // Skip tokens until LBrace or next declaration
12169        while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
12170            if self.check(TokenType::Eof) {
12171                break;
12172            }
12173            self.advance();
12174        }
12175
12176        // Skip braced block if present
12177        if self.check(TokenType::LBrace) {
12178            self.skip_braced_block()?;
12179        }
12180
12181        Ok(Declaration::Generic(GenericDeclaration {
12182            keyword: kw_tok.value,
12183            name,
12184            loc: Loc {
12185                line: kw_tok.line,
12186                column: kw_tok.column,
12187            },
12188            leading_trivia: Vec::new(),
12189            trailing_trivia: Vec::new(),
12190        }))
12191    }
12192
12193    // ──────────────────────────────────────────────────────────────────
12194    // v1.6.0 — Mobile Typed Channels parsers
12195    // (paper_mobile_channels.md section 3 + plan/the design plan)
12196    //  Direct port of axon/compiler/parser.py:_parse_channel/emit/publish/discover.
12197    // ──────────────────────────────────────────────────────────────────
12198
12199    /// Parse: `channel Name { message, qos, lifetime, persistence, shield }`.
12200    fn parse_channel(&mut self) -> Result<ChannelDefinition, ParseError> {
12201        let tok = self.consume(TokenType::Channel)?;
12202        let name = self.consume(TokenType::Identifier)?.value;
12203        let mut node = ChannelDefinition {
12204            name: name.clone(),
12205            message: String::new(),
12206            qos: "at_least_once".to_string(),
12207            lifetime: "affine".to_string(),
12208            persistence: "ephemeral".to_string(),
12209            shield_ref: String::new(),
12210            loc: Loc {
12211                line: tok.line,
12212                column: tok.column,
12213            },
12214            leading_trivia: Vec::new(),
12215            trailing_trivia: Vec::new(),
12216        };
12217        self.consume(TokenType::LBrace)?;
12218        while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
12219            let field_tok = self.current().clone();
12220            let field_name = field_tok.value.clone();
12221            self.advance();
12222            if !self.check(TokenType::Colon) {
12223                if self.check(TokenType::LBrace) {
12224                    self.skip_braced_block()?;
12225                }
12226                continue;
12227            }
12228            self.advance();
12229            match field_name.as_str() {
12230                "message" => node.message = self.parse_channel_message_type()?,
12231                "qos" => {
12232                    let q_tok = self.consume_any_ident_or_kw()?;
12233                    if !matches!(
12234                        q_tok.value.as_str(),
12235                        "at_most_once" | "at_least_once" | "exactly_once" | "broadcast" | "queue"
12236                    ) {
12237                        return Err(ParseError {
12238                            message: format!(
12239                                "Invalid qos '{}' in channel '{}' — \
12240                                 expected at_most_once | at_least_once | \
12241                                 exactly_once | broadcast | queue",
12242                                q_tok.value, name
12243                            ),
12244                            line: q_tok.line,
12245                            column: q_tok.column,
12246                                                    ..Default::default()
12247                        });
12248                    }
12249                    node.qos = q_tok.value;
12250                }
12251                "lifetime" => {
12252                    let lt_tok = self.consume_any_ident_or_kw()?;
12253                    if !matches!(lt_tok.value.as_str(), "linear" | "affine" | "persistent") {
12254                        return Err(ParseError {
12255                            message: format!(
12256                                "Invalid lifetime '{}' in channel '{}' — \
12257                                 expected linear | affine | persistent",
12258                                lt_tok.value, name
12259                            ),
12260                            line: lt_tok.line,
12261                            column: lt_tok.column,
12262                                                    ..Default::default()
12263                        });
12264                    }
12265                    node.lifetime = lt_tok.value;
12266                }
12267                "persistence" => {
12268                    let p_tok = self.consume_any_ident_or_kw()?;
12269                    if !matches!(p_tok.value.as_str(), "ephemeral" | "persistent_axonstore") {
12270                        return Err(ParseError {
12271                            message: format!(
12272                                "Invalid persistence '{}' in channel '{}' — \
12273                                 expected ephemeral | persistent_axonstore",
12274                                p_tok.value, name
12275                            ),
12276                            line: p_tok.line,
12277                            column: p_tok.column,
12278                                                    ..Default::default()
12279                        });
12280                    }
12281                    node.persistence = p_tok.value;
12282                }
12283                "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
12284                _ => self.skip_value(),
12285            }
12286        }
12287        self.consume(TokenType::RBrace)?;
12288        Ok(node)
12289    }
12290
12291    /// Parse a `message:` value, supporting nested `Channel<…>`
12292    /// (second-order session types — paper section 3.3).
12293    fn parse_channel_message_type(&mut self) -> Result<String, ParseError> {
12294        let head = self.consume(TokenType::Identifier)?;
12295        let mut spelling = head.value;
12296        if self.check(TokenType::Lt) {
12297            self.advance();
12298            let inner = self.parse_channel_message_type()?;
12299            self.consume(TokenType::Gt)?;
12300            spelling = format!("{}<{}>", spelling, inner);
12301        }
12302        Ok(spelling)
12303    }
12304
12305    /// Parse: `emit ChannelName(value_ref)` — Chan-Output / Chan-Mobility.
12306    ///
12307    /// `value_ref` accepts a bare identifier (variable / channel name for
12308    /// mobility) or a dotted path (`Step.output.field`) referencing a prior
12309    /// step result (v1.6.0 — runtime resolves via ContextManager).
12310    fn parse_emit_step(&mut self) -> Result<FlowStep, ParseError> {
12311        let tok = self.consume(TokenType::Emit)?;
12312        let channel = self.consume(TokenType::Identifier)?.value;
12313        self.consume(TokenType::LParen)?;
12314        let value = self.parse_emit_value_ref()?;
12315        self.consume(TokenType::RParen)?;
12316        Ok(FlowStep::Emit(EmitStatement {
12317            channel_ref: channel,
12318            value_ref: value,
12319            loc: Loc {
12320                line: tok.line,
12321                column: tok.column,
12322            },
12323        }))
12324    }
12325
12326    /// v2.46.0 — parse `mint <Credential> as <binding>`. The credential
12327    /// reference must resolve to a declared `credential` (`axon-T895`,
12328    /// type-checker); the binding is a fresh flow-scoped name receiving the
12329    /// raw bearer string. Both tokens are required — a `mint` with no
12330    /// binding would mint authority into the void.
12331    fn parse_mint_step(&mut self) -> Result<FlowStep, ParseError> {
12332        let tok = self.consume(TokenType::Mint)?;
12333        let credential_ref = self.consume(TokenType::Identifier)?.value;
12334        self.consume(TokenType::As)?;
12335        let binding = self.consume(TokenType::Identifier)?.value;
12336        Ok(FlowStep::Mint(MintStep {
12337            credential_ref,
12338            binding,
12339            loc: Loc {
12340                line: tok.line,
12341                column: tok.column,
12342            },
12343        }))
12344    }
12345
12346    /// v2.48.0 — parse `rotate <SecretsStore> [where "<filter>"] with
12347    /// <Tool> as <binding>` (doctrine `rotation_without_revelation`).
12348    ///
12349    /// All three anchors are grammar, not convention: the store names WHAT
12350    /// may rotate (a `backend: secrets` class view — `axon-T898` in the
12351    /// type-checker), the tool names WHO performs the exchange
12352    /// (`axon-T899`), and the binding receives the metadata-only summary —
12353    /// a `rotate` without a binding would renew authority with no
12354    /// observable outcome, so `as` is REQUIRED (the `mint` posture). The
12355    /// `where` filter is optional (v2.21.0 string grammar, proven against the
12356    /// synthesized metadata schema); omitting it rotates the WHOLE class —
12357    /// the deliberate post-breach bulk shape. `with` is a soft keyword
12358    /// (not a lexer token): reserving it globally would break every
12359    /// adopter identifier named `with`.
12360    fn parse_rotate_step(&mut self) -> Result<FlowStep, ParseError> {
12361        let tok = self.consume(TokenType::Rotate)?;
12362        let store_ref = self.consume(TokenType::Identifier)?.value;
12363        let mut where_expr = String::new();
12364        if self.check(TokenType::Where) {
12365            self.advance();
12366            where_expr = self.consume(TokenType::StringLit)?.value.clone();
12367        }
12368        let with_tok = self.current().clone();
12369        if with_tok.value != "with" {
12370            return Err(ParseError {
12371                message: format!(
12372                    "Expected `with <Tool>` after `rotate {store_ref}{}`, found '{}'. \
12373                     A rotation names the tool that performs the renewal exchange: \
12374                     `rotate {store_ref} [where \"<filter>\"] with <Tool> as <binding>`.",
12375                    if where_expr.is_empty() { "" } else { " where …" },
12376                    with_tok.value
12377                ),
12378                line: with_tok.line,
12379                column: with_tok.column,
12380                ..Default::default()
12381            });
12382        }
12383        self.advance();
12384        let tool_ref = self.consume(TokenType::Identifier)?.value;
12385        self.consume(TokenType::As)?;
12386        let binding = self.consume(TokenType::Identifier)?.value;
12387        Ok(FlowStep::Rotate(RotateStep {
12388            store_ref,
12389            where_expr,
12390            tool_ref,
12391            binding,
12392            loc: Loc {
12393                line: tok.line,
12394                column: tok.column,
12395            },
12396        }))
12397    }
12398
12399    /// Parse: `IDENTIFIER ('.' (IDENTIFIER | keyword))*` → dot-joined string
12400    /// (v1.6.0).
12401    ///
12402    /// Mirrors the Python `_parse_emit_value_ref` helper exactly so the IR
12403    /// JSON for `emit Hello(Build.output)` is byte-identical between the
12404    /// two reference implementations.
12405    ///
12406    /// The HEAD must be a real ``Identifier``. Subsequent segments after a
12407    /// `.` may be identifiers OR keywords — common field names like
12408    /// ``output``, ``result``, ``message``, ``state``, etc. are reserved
12409    /// words in Axon but adopters must be able to write them as
12410    /// dotted-access segments. The accepting predicate:
12411    ///   - the lexer carried a non-empty `value` (every Word-like token does)
12412    ///   - the value's first byte is a letter or underscore (filters out
12413    ///     punctuation tokens such as ',', '{', etc.)
12414    fn parse_emit_value_ref(&mut self) -> Result<String, ParseError> {
12415        let head = self.consume(TokenType::Identifier)?.value;
12416        let mut parts = vec![head];
12417        while self.check(TokenType::Dot) {
12418            self.advance(); // consume '.'
12419            let next_tok = self.current().clone();
12420            let valid = !next_tok.value.is_empty()
12421                && next_tok.value.as_bytes()[0].is_ascii_alphabetic()
12422                || next_tok.value.starts_with('_');
12423            if !valid {
12424                return Err(ParseError {
12425                    message: format!(
12426                        "Expected identifier or keyword after '.' in dotted \
12427                         access, found {:?}",
12428                        next_tok.value
12429                    ),
12430                    line: next_tok.line,
12431                    column: next_tok.column,
12432                                    ..Default::default()
12433                });
12434            }
12435            self.advance();
12436            parts.push(next_tok.value);
12437        }
12438        Ok(parts.join("."))
12439    }
12440
12441    /// Parse: `publish ChannelName within ShieldName` — Publish-Ext (D8).
12442    fn parse_publish_step(&mut self) -> Result<FlowStep, ParseError> {
12443        let tok = self.consume(TokenType::Publish)?;
12444        let channel = self.consume(TokenType::Identifier)?.value;
12445        self.consume(TokenType::Within)?;
12446        let shield = self.consume(TokenType::Identifier)?.value;
12447        Ok(FlowStep::Publish(PublishStatement {
12448            channel_ref: channel,
12449            shield_ref: shield,
12450            loc: Loc {
12451                line: tok.line,
12452                column: tok.column,
12453            },
12454        }))
12455    }
12456
12457    /// Parse: `discover ChannelName as alias` — dual of publish.
12458    fn parse_discover_step(&mut self) -> Result<FlowStep, ParseError> {
12459        let tok = self.consume(TokenType::Discover)?;
12460        let cap = self.consume(TokenType::Identifier)?.value;
12461        self.consume(TokenType::As)?;
12462        let alias = self.consume(TokenType::Identifier)?.value;
12463        Ok(FlowStep::Discover(DiscoverStatement {
12464            capability_ref: cap,
12465            alias,
12466            loc: Loc {
12467                line: tok.line,
12468                column: tok.column,
12469            },
12470        }))
12471    }
12472}
12473
12474// ── v1.6.0 — Mobile Typed Channels parser tests ─────────────────────
12475
12476#[cfg(test)]
12477mod parser_tests {
12478    use super::*;
12479    use crate::lexer::Lexer;
12480
12481    fn parse(src: &str) -> Result<Program, ParseError> {
12482        let tokens = Lexer::new(src, "<test>").tokenize().expect("lex");
12483        Parser::new(tokens).parse()
12484    }
12485
12486    #[test]
12487    fn channel_full_parses() {
12488        let src = r#"channel C { message: Order qos: at_least_once lifetime: affine persistence: ephemeral shield: Gate }"#;
12489        let prog = parse(src).expect("parse");
12490        match &prog.declarations[0] {
12491            Declaration::Channel(c) => {
12492                assert_eq!(c.name, "C");
12493                assert_eq!(c.message, "Order");
12494                assert_eq!(c.qos, "at_least_once");
12495                assert_eq!(c.lifetime, "affine");
12496                assert_eq!(c.persistence, "ephemeral");
12497                assert_eq!(c.shield_ref, "Gate");
12498            }
12499            _ => panic!("expected ChannelDefinition"),
12500        }
12501    }
12502
12503    #[test]
12504    fn channel_defaults_match_paper_d1() {
12505        let prog = parse("channel C { message: Order }").expect("parse");
12506        if let Declaration::Channel(c) = &prog.declarations[0] {
12507            assert_eq!(c.qos, "at_least_once"); // default
12508            assert_eq!(c.lifetime, "affine"); // D1 default
12509            assert_eq!(c.persistence, "ephemeral");
12510            assert_eq!(c.shield_ref, "");
12511        } else {
12512            panic!("expected ChannelDefinition");
12513        }
12514    }
12515
12516    #[test]
12517    fn channel_second_order_message_type_parses() {
12518        let prog = parse("channel C { message: Channel<Order> }").expect("parse");
12519        if let Declaration::Channel(c) = &prog.declarations[0] {
12520            assert_eq!(c.message, "Channel<Order>");
12521        } else {
12522            panic!("expected ChannelDefinition");
12523        }
12524    }
12525
12526    #[test]
12527    fn channel_nested_channel_message_type_parses() {
12528        let prog = parse("channel C { message: Channel<Channel<Order>> }").expect("parse");
12529        if let Declaration::Channel(c) = &prog.declarations[0] {
12530            assert_eq!(c.message, "Channel<Channel<Order>>");
12531        } else {
12532            panic!("expected ChannelDefinition");
12533        }
12534    }
12535
12536    #[test]
12537    fn channel_invalid_qos_rejected() {
12538        let err = parse("channel C { message: T qos: bogus }").unwrap_err();
12539        assert!(err.message.contains("Invalid qos"), "got {}", err.message);
12540    }
12541
12542    #[test]
12543    fn channel_invalid_lifetime_rejected() {
12544        let err = parse("channel C { message: T lifetime: eternal }").unwrap_err();
12545        assert!(
12546            err.message.contains("Invalid lifetime"),
12547            "got {}",
12548            err.message
12549        );
12550    }
12551
12552    #[test]
12553    fn channel_invalid_persistence_rejected() {
12554        let err = parse("channel C { message: T persistence: forever }").unwrap_err();
12555        assert!(
12556            err.message.contains("Invalid persistence"),
12557            "got {}",
12558            err.message
12559        );
12560    }
12561
12562    #[test]
12563    fn emit_value_parses() {
12564        let src = "flow f() -> Out { emit C(payload) }";
12565        let prog = parse(src).expect("parse");
12566        if let Declaration::Flow(f) = &prog.declarations[0] {
12567            match &f.body[0] {
12568                FlowStep::Emit(e) => {
12569                    assert_eq!(e.channel_ref, "C");
12570                    assert_eq!(e.value_ref, "payload");
12571                }
12572                other => panic!("expected Emit, got {:?}", other),
12573            }
12574        } else {
12575            panic!("expected Flow");
12576        }
12577    }
12578
12579    #[test]
12580    fn publish_within_shield_parses() {
12581        let src = "flow f() -> Cap { publish C within Gate }";
12582        let prog = parse(src).expect("parse");
12583        if let Declaration::Flow(f) = &prog.declarations[0] {
12584            match &f.body[0] {
12585                FlowStep::Publish(p) => {
12586                    assert_eq!(p.channel_ref, "C");
12587                    assert_eq!(p.shield_ref, "Gate");
12588                }
12589                other => panic!("expected Publish, got {:?}", other),
12590            }
12591        } else {
12592            panic!("expected Flow");
12593        }
12594    }
12595
12596    #[test]
12597    fn discover_with_alias_parses() {
12598        let src = "flow f() -> Out { discover C as ch }";
12599        let prog = parse(src).expect("parse");
12600        if let Declaration::Flow(f) = &prog.declarations[0] {
12601            match &f.body[0] {
12602                FlowStep::Discover(d) => {
12603                    assert_eq!(d.capability_ref, "C");
12604                    assert_eq!(d.alias, "ch");
12605                }
12606                other => panic!("expected Discover, got {:?}", other),
12607            }
12608        } else {
12609            panic!("expected Flow");
12610        }
12611    }
12612
12613    #[test]
12614    fn listen_typed_ref_sets_flag_true() {
12615        let src = "daemon D() { goal: \"x\" listen C as ev { } }";
12616        let prog = parse(src).expect("parse");
12617        if let Declaration::Daemon(d) = &prog.declarations[0] {
12618            assert_eq!(d.listeners.len(), 1);
12619            assert_eq!(d.listeners[0].channel, "C");
12620            assert!(d.listeners[0].channel_is_ref, "typed ref ⇒ true");
12621        } else {
12622            panic!("expected Daemon");
12623        }
12624    }
12625
12626    #[test]
12627    fn listen_string_topic_legacy_flag_false() {
12628        let src = "daemon D() { goal: \"x\" listen \"orders\" as ev { } }";
12629        let prog = parse(src).expect("parse");
12630        if let Declaration::Daemon(d) = &prog.declarations[0] {
12631            assert_eq!(d.listeners.len(), 1);
12632            assert_eq!(d.listeners[0].channel, "orders");
12633            assert!(!d.listeners[0].channel_is_ref, "string topic ⇒ false");
12634        } else {
12635            panic!("expected Daemon");
12636        }
12637    }
12638
12639    // ── v1.6.0 — emit value_ref accepts dotted access ───────────
12640
12641    fn extract_first_emit(prog: &Program) -> &EmitStatement {
12642        if let Declaration::Flow(f) = &prog.declarations[0] {
12643            if let FlowStep::Emit(e) = &f.body[0] {
12644                return e;
12645            }
12646        }
12647        panic!("expected emit statement at flow body[0]");
12648    }
12649
12650    #[test]
12651    fn emit_accepts_bare_identifier_value_ref() {
12652        // Pre-13.i baseline — must keep working.
12653        let prog = parse("flow f() -> Out { emit Hello(payload) }").expect("parse");
12654        let emit = extract_first_emit(&prog);
12655        assert_eq!(emit.channel_ref, "Hello");
12656        assert_eq!(emit.value_ref, "payload");
12657    }
12658
12659    #[test]
12660    fn emit_accepts_two_segment_dotted_value_ref() {
12661        // The exact case adopters reported as broken before 13.i.
12662        let prog = parse("flow f() -> Out { emit Hello(Build.output) }").expect("parse");
12663        let emit = extract_first_emit(&prog);
12664        assert_eq!(emit.value_ref, "Build.output");
12665    }
12666
12667    #[test]
12668    fn emit_accepts_three_segment_nested_dotted_value_ref() {
12669        let prog = parse("flow f() -> Out { emit Score(Analyze.result.score) }").expect("parse");
12670        let emit = extract_first_emit(&prog);
12671        assert_eq!(emit.value_ref, "Analyze.result.score");
12672    }
12673
12674    #[test]
12675    fn emit_dotted_with_trailing_dot_fails() {
12676        // Trailing `.` must still error — every '.' demands an identifier.
12677        let result = parse("flow f() -> Out { emit Hello(Build.) }");
12678        assert!(result.is_err(), "expected parse error for trailing dot");
12679    }
12680}
12681
12682// ── v1.5.2 — declaration_trivia parallel channel tests ──────────────────
12683
12684#[cfg(test)]
12685mod declaration_trivia_tests {
12686    use super::*;
12687    use crate::lexer::Lexer;
12688    use crate::tokens::TriviaKind;
12689
12690    fn parse(src: &str) -> Program {
12691        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
12692        Parser::new(toks).parse().expect("parse")
12693    }
12694
12695    #[test]
12696    fn no_comments_means_empty_trivia_per_decl() {
12697        let prog = parse("flow F() -> Out { }");
12698        assert_eq!(prog.declarations.len(), 1);
12699        assert_eq!(prog.declaration_trivia.len(), 1);
12700        assert!(prog.declaration_trivia[0].leading.is_empty());
12701        assert!(prog.declaration_trivia[0].trailing.is_empty());
12702    }
12703
12704    #[test]
12705    fn doc_line_comment_attaches_as_leading() {
12706        let prog = parse("/// Documents F\nflow F() -> Out { }");
12707        let triv = &prog.declaration_trivia[0];
12708        assert_eq!(triv.leading.len(), 1);
12709        assert_eq!(triv.leading[0].kind, TriviaKind::DocLine);
12710        assert!(triv.leading[0].is_doc());
12711        assert_eq!(triv.leading[0].text, "/// Documents F");
12712    }
12713
12714    #[test]
12715    fn regular_line_comment_attaches_as_leading() {
12716        let prog = parse("// header\nflow F() -> Out { }");
12717        let triv = &prog.declaration_trivia[0];
12718        assert_eq!(triv.leading.len(), 1);
12719        assert_eq!(triv.leading[0].kind, TriviaKind::Line);
12720        assert!(!triv.leading[0].is_doc());
12721    }
12722
12723    #[test]
12724    fn block_doc_comment_attaches_as_leading() {
12725        let prog = parse("/** Doc block */\nflow F() -> Out { }");
12726        let triv = &prog.declaration_trivia[0];
12727        assert_eq!(triv.leading[0].kind, TriviaKind::DocBlock);
12728        assert!(triv.leading[0].is_doc());
12729    }
12730
12731    #[test]
12732    fn multiple_comments_collected_in_source_order() {
12733        let src = "/// First\n/// Second\nflow F() -> Out { }";
12734        let prog = parse(src);
12735        let triv = &prog.declaration_trivia[0];
12736        assert_eq!(triv.leading.len(), 2);
12737        assert_eq!(triv.leading[0].text, "/// First");
12738        assert_eq!(triv.leading[1].text, "/// Second");
12739    }
12740
12741    #[test]
12742    fn three_decls_each_get_own_leading() {
12743        let src = "/// for A\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }\n/// for C\nflow C() -> Out { }";
12744        let prog = parse(src);
12745        assert_eq!(prog.declarations.len(), 3);
12746        assert_eq!(prog.declaration_trivia.len(), 3);
12747        for (idx, name) in ["A", "B", "C"].iter().enumerate() {
12748            let triv = &prog.declaration_trivia[idx];
12749            assert_eq!(triv.leading.len(), 1);
12750            assert_eq!(triv.leading[0].text, format!("/// for {name}"));
12751        }
12752    }
12753
12754    #[test]
12755    fn trailing_comment_attaches_to_last_token_of_decl() {
12756        // Comment on the same line as the decl's closing brace.
12757        let prog = parse("flow F() -> Out { } // tail");
12758        let triv = &prog.declaration_trivia[0];
12759        assert_eq!(triv.trailing.len(), 1);
12760        assert_eq!(triv.trailing[0].text, "// tail");
12761    }
12762
12763    #[test]
12764    fn mixed_doc_and_regular_preserve_order_between_decls() {
12765        let src = "/// doc for A\nflow A() -> Out { }\n\n// header line\n/// doc for B\nflow B() -> Out { }";
12766        let prog = parse(src);
12767        assert_eq!(prog.declarations.len(), 2);
12768        // A: just the doc comment.
12769        assert_eq!(prog.declaration_trivia[0].leading.len(), 1);
12770        // B: header + doc, in source order.
12771        assert_eq!(prog.declaration_trivia[1].leading.len(), 2);
12772        assert_eq!(prog.declaration_trivia[1].leading[0].text, "// header line");
12773        assert_eq!(prog.declaration_trivia[1].leading[1].text, "/// doc for B");
12774    }
12775
12776    #[test]
12777    fn parser_unaffected_by_comments_in_grammar_path() {
12778        // The parser must accept comments interleaved between every
12779        // legal token without affecting the AST shape it produces.
12780        // This is the regression guard for "lossless lexing must not
12781        // change parsing semantics."
12782        let src =
12783            "// before flow\nflow /* between flow and name */ F() -> Out {\n  // body comment\n}";
12784        let prog = parse(src);
12785        assert_eq!(prog.declarations.len(), 1);
12786        if let Declaration::Flow(f) = &prog.declarations[0] {
12787            assert_eq!(f.name, "F");
12788        } else {
12789            panic!("expected Flow declaration");
12790        }
12791    }
12792}
12793
12794// ── v1.5.2 — per-struct trivia fields tests ─────────────────────────────
12795//
12796// 14.b spreads `leading_trivia` / `trailing_trivia` into every Declaration
12797// variant struct (FlowDefinition, ChannelDefinition, PersonaDefinition, …).
12798// The Python AST already had this shape since 14.a; 14.b achieves Rust
12799// parity. The side-channel `Program.declaration_trivia` is preserved for
12800// backward compat — these tests verify the new direct access path.
12801
12802#[cfg(test)]
12803mod per_struct_trivia_tests {
12804    use super::*;
12805    use crate::lexer::Lexer;
12806    use crate::tokens::TriviaKind;
12807
12808    fn parse(src: &str) -> Program {
12809        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
12810        Parser::new(toks).parse().expect("parse")
12811    }
12812
12813    #[test]
12814    fn flow_definition_carries_leading_trivia_directly() {
12815        let prog = parse("/// documents F\nflow F() -> Out { }");
12816        if let Declaration::Flow(f) = &prog.declarations[0] {
12817            assert_eq!(f.leading_trivia.len(), 1);
12818            assert_eq!(f.leading_trivia[0].kind, TriviaKind::DocLine);
12819            assert_eq!(f.leading_trivia[0].text, "/// documents F");
12820            assert!(f.trailing_trivia.is_empty());
12821        } else {
12822            panic!("expected Flow declaration");
12823        }
12824    }
12825
12826    #[test]
12827    fn flow_definition_carries_trailing_trivia_directly() {
12828        let prog = parse("flow F() -> Out { } // tail comment");
12829        if let Declaration::Flow(f) = &prog.declarations[0] {
12830            assert_eq!(f.trailing_trivia.len(), 1);
12831            assert_eq!(f.trailing_trivia[0].text, "// tail comment");
12832        } else {
12833            panic!("expected Flow declaration");
12834        }
12835    }
12836
12837    #[test]
12838    fn channel_definition_carries_trivia_directly() {
12839        // ChannelDefinition is a Tier-1 declaration; verify per-struct fields
12840        // populate just like FlowDefinition.
12841        let src = concat!(
12842            "/// inbound order events\n",
12843            "channel Orders {\n",
12844            "    message:     Order\n",
12845            "    qos:         at_least_once\n",
12846            "    lifetime:    affine\n",
12847            "    persistence: ephemeral\n",
12848            "    shield:      Broker\n",
12849            "}",
12850        );
12851        let prog = parse(src);
12852        if let Declaration::Channel(ch) = &prog.declarations[0] {
12853            assert_eq!(ch.leading_trivia.len(), 1);
12854            assert!(ch.leading_trivia[0].is_doc());
12855            assert_eq!(ch.leading_trivia[0].text, "/// inbound order events");
12856        } else {
12857            panic!("expected Channel declaration");
12858        }
12859    }
12860
12861    #[test]
12862    fn per_struct_fields_match_side_channel() {
12863        // 14.a side-channel and 14.b per-struct fields must hold identical
12864        // data — they are populated by the same parser pass.
12865        let src = "/// for A\n// header for B\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }";
12866        let prog = parse(src);
12867        for (idx, decl) in prog.declarations.iter().enumerate() {
12868            let side = &prog.declaration_trivia[idx];
12869            let (per_lead, per_trail) = match decl {
12870                Declaration::Flow(f) => (&f.leading_trivia, &f.trailing_trivia),
12871                _ => panic!("unexpected variant"),
12872            };
12873            assert_eq!(per_lead.len(), side.leading.len());
12874            assert_eq!(per_trail.len(), side.trailing.len());
12875            for (a, b) in per_lead.iter().zip(side.leading.iter()) {
12876                assert_eq!(a.text, b.text);
12877                assert_eq!(a.kind, b.kind);
12878            }
12879        }
12880    }
12881
12882    #[test]
12883    fn comment_free_program_yields_empty_per_struct_fields() {
12884        let prog = parse("flow F() -> Out { }");
12885        if let Declaration::Flow(f) = &prog.declarations[0] {
12886            assert!(f.leading_trivia.is_empty());
12887            assert!(f.trailing_trivia.is_empty());
12888        } else {
12889            panic!("expected Flow declaration");
12890        }
12891    }
12892}
12893
12894// ── v1.5.2 — inner doc comments (//!, /*!) ──────────────────────────────
12895//
12896// Inner doc comments document the *enclosing* item rather than the next
12897// sibling. Today they flow through the trivia channel like any other
12898// comment; downstream consumers (axon doc, LSP) decide how to interpret
12899// `is_inner_doc()`. These tests verify the lexer→parser pipeline preserves
12900// the inner-doc discriminator end-to-end.
12901
12902#[cfg(test)]
12903mod inner_doc_tests {
12904    use super::*;
12905    use crate::lexer::Lexer;
12906    use crate::tokens::TriviaKind;
12907
12908    fn parse(src: &str) -> Program {
12909        let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
12910        Parser::new(toks).parse().expect("parse")
12911    }
12912
12913    #[test]
12914    fn inner_doc_line_reaches_leading_trivia() {
12915        let src = "//! file-level docs\nflow F() -> Out { }";
12916        let prog = parse(src);
12917        let triv = &prog.declaration_trivia[0];
12918        assert_eq!(triv.leading.len(), 1);
12919        assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocLine);
12920        assert!(triv.leading[0].is_doc());
12921        assert!(triv.leading[0].is_inner_doc());
12922        assert_eq!(triv.leading[0].text, "//! file-level docs");
12923        assert_eq!(triv.leading[0].stripped_text(), " file-level docs");
12924    }
12925
12926    #[test]
12927    fn inner_doc_block_reaches_leading_trivia() {
12928        let src = "/*! module-level docs */\nflow F() -> Out { }";
12929        let prog = parse(src);
12930        let triv = &prog.declaration_trivia[0];
12931        assert_eq!(triv.leading.len(), 1);
12932        assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocBlock);
12933        assert!(triv.leading[0].is_inner_doc());
12934        assert_eq!(triv.leading[0].stripped_text(), " module-level docs ");
12935    }
12936
12937    #[test]
12938    fn outer_and_inner_doc_can_coexist() {
12939        // File-level inner doc on top, then an outer doc for the
12940        // declaration. Both reach the trivia channel and remain
12941        // distinguishable via `is_inner_doc()`.
12942        let src = "//! file docs\n/// docs F\nflow F() -> Out { }";
12943        let prog = parse(src);
12944        let triv = &prog.declaration_trivia[0];
12945        assert_eq!(triv.leading.len(), 2);
12946        assert!(triv.leading[0].is_inner_doc());
12947        assert!(triv.leading[1].is_doc());
12948        assert!(!triv.leading[1].is_inner_doc());
12949    }
12950
12951    #[test]
12952    fn inner_doc_reaches_per_struct_fields() {
12953        // Same data must be visible via the per-struct fields (v1.5.2).
12954        let src = "//! intro\nflow F() -> Out { }";
12955        let prog = parse(src);
12956        if let Declaration::Flow(f) = &prog.declarations[0] {
12957            assert_eq!(f.leading_trivia.len(), 1);
12958            assert!(f.leading_trivia[0].is_inner_doc());
12959        } else {
12960            panic!("expected Flow declaration");
12961        }
12962    }
12963}
12964
12965// ── v1.20.0 — Parser error recovery test pack ─────────────────────────────
12966//
12967// Mirror of `tests/test_fase28_parser_recovery.py` (Python side, 28.b).
12968// The test classes here line up 1-1 with the Python ones so the cross-
12969// stack drift gate (28.i) can compare error-list shapes input-for-input.
12970//
12971// Test classes:
12972//   - backwards_compat: existing `parse()` API unchanged
12973//   - single_error_recovery: one bad decl → one error, rest parse OK
12974//   - multi_error_recovery: N independent errors → N entries
12975//   - sync_points: every top-level keyword resyncs correctly
12976//   - parse_result_api: `has_errors`, `is_clean`
12977//   - edge_cases: EOF mid-error, brace imbalance, only-bad-tokens
12978//   - robustness_fuzz: 1000 deterministic-seeded mutations never crash
12979//   - no_ghost_errors: single broken field produces exactly 1 error
12980//   - integration_with_colon_diagnostic: v1.19.4 hint preserved under
12981//     recovery mode
12982#[cfg(test)]
12983mod recovery_tests {
12984    use super::*;
12985    use crate::lexer::Lexer;
12986
12987    /// Lex a source and return tokens for the parser to consume.
12988    /// Mirrors the Python `_parse_recovery` helper.
12989    fn lex(src: &str) -> Vec<Token> {
12990        Lexer::new(src, "<test>").tokenize().expect("lex")
12991    }
12992
12993    /// Parse with recovery mode. Returns `(program, errors)` so call
12994    /// sites read like the Python helper.
12995    fn recover(src: &str) -> ParseResult {
12996        Parser::new(lex(src)).parse_with_recovery()
12997    }
12998
12999    /// Strict parse. Mirrors the Python `_parse_strict` helper.
13000    fn strict(src: &str) -> Result<Program, ParseError> {
13001        Parser::new(lex(src)).parse()
13002    }
13003
13004    // ── backwards_compat ─────────────────────────────────────────
13005
13006    #[test]
13007    fn strict_parse_unchanged_for_clean_source() {
13008        // The existing `parse()` API must continue to succeed
13009        // verbatim on every well-formed input — D9.
13010        let src = "intent I {}";
13011        let prog = strict(src).expect("clean parse");
13012        assert_eq!(prog.declarations.len(), 1);
13013    }
13014
13015    #[test]
13016    fn strict_parse_still_raises_on_first_error() {
13017        // D9 + D8: opt-in to recovery via `parse_with_recovery`;
13018        // strict mode must still bubble the first error.
13019        // (Using a parse-time error rather than a lex error — `@@@`
13020        // would be rejected by the lexer, which is out of scope.)
13021        let src = "flow F() { } not_a_keyword flow G() { }";
13022        let _ = strict(src).expect_err("must error fast in strict mode");
13023    }
13024
13025    #[test]
13026    fn recovery_clean_source_yields_no_errors() {
13027        let src = "flow F() { } flow G() { }";
13028        let pr = recover(src);
13029        assert!(pr.is_clean(), "errors: {:?}", pr.errors);
13030        assert_eq!(pr.program.declarations.len(), 2);
13031    }
13032
13033    // ── single_error_recovery ────────────────────────────────────
13034
13035    #[test]
13036    fn single_unknown_top_level_token_recovers() {
13037        // One garbage token at top level; rest must parse.
13038        let src = "garbage_token flow F() { } flow G() { }";
13039        let pr = recover(src);
13040        assert_eq!(pr.errors.len(), 1, "errors: {:?}", pr.errors);
13041        assert_eq!(pr.program.declarations.len(), 2);
13042    }
13043
13044    #[test]
13045    fn error_in_first_decl_does_not_block_second() {
13046        // `flow F` body refers to non-keyword `nope`; the error
13047        // recovery must skip to the next top-level keyword.
13048        let src = "flow F() { not_a_step nope } flow G() { }";
13049        let pr = recover(src);
13050        assert!(pr.has_errors(), "expected at least one error");
13051        // The second flow must be reachable.
13052        let names: Vec<&str> = pr
13053            .program
13054            .declarations
13055            .iter()
13056            .filter_map(|d| match d {
13057                Declaration::Flow(f) => Some(f.name.as_str()),
13058                _ => None,
13059            })
13060            .collect();
13061        assert!(names.contains(&"G"), "G not found among {names:?}");
13062    }
13063
13064    #[test]
13065    fn malformed_declaration_then_clean_intent_recovers() {
13066        let src = "flow @ () { } intent I {}";
13067        let pr = recover(src);
13068        assert!(pr.has_errors());
13069        let kinds: Vec<&str> = pr
13070            .program
13071            .declarations
13072            .iter()
13073            .map(|d| match d {
13074                Declaration::Intent(_) => "intent",
13075                Declaration::Flow(_) => "flow",
13076                _ => "other",
13077            })
13078            .collect();
13079        assert!(kinds.contains(&"intent"), "kinds: {kinds:?}");
13080    }
13081
13082    #[test]
13083    fn recovery_does_not_double_count_a_single_error() {
13084        // Regression for the "ghost error" pathology that surfaced
13085        // during 28.b dev: a nested-decl error must not also fire
13086        // an "Unexpected token at top level" from the outer loop.
13087        // The Rust grammar has stricter intra-flow requirements
13088        // than Python; the invariant we assert here is that the
13089        // outer loop emits zero "Unexpected token at top level"
13090        // errors after an inner step-shape error.
13091        let src = "flow F() { not_a_step }";
13092        let pr = recover(src);
13093        let outer_ghosts = pr
13094            .errors
13095            .iter()
13096            .filter(|e| e.message.contains("at top level"))
13097            .count();
13098        assert_eq!(outer_ghosts, 0, "ghost errors: {:?}", pr.errors);
13099    }
13100
13101    // ── multi_error_recovery ─────────────────────────────────────
13102
13103    #[test]
13104    fn three_independent_errors_yield_three_entries() {
13105        let src =
13106            "garbage1 flow F() { } garbage2 flow G() { } garbage3 flow H() { }";
13107        let pr = recover(src);
13108        assert_eq!(pr.errors.len(), 3, "errors: {:?}", pr.errors);
13109        assert_eq!(pr.program.declarations.len(), 3);
13110    }
13111
13112    #[test]
13113    fn all_errors_no_valid_declarations() {
13114        let src = "foo bar baz qux";
13115        let pr = recover(src);
13116        assert!(pr.has_errors());
13117        assert!(pr.program.declarations.is_empty());
13118    }
13119
13120    #[test]
13121    fn errors_recorded_in_source_order() {
13122        let src = "x flow A() { } y flow B() { } z flow C() { }";
13123        let pr = recover(src);
13124        assert_eq!(pr.errors.len(), 3);
13125        let lines: Vec<u32> = pr.errors.iter().map(|e| e.line).collect();
13126        // Same source-line means we compare by column ordering;
13127        // either way they must be non-decreasing.
13128        assert!(
13129            lines.windows(2).all(|w| w[0] <= w[1]),
13130            "errors out of order: {lines:?}"
13131        );
13132    }
13133
13134    // ── sync_points ──────────────────────────────────────────────
13135
13136    #[test]
13137    fn sync_to_flow_keyword() {
13138        let src = "garbage flow F() { }";
13139        let pr = recover(src);
13140        assert_eq!(pr.program.declarations.len(), 1);
13141    }
13142
13143    #[test]
13144    fn sync_to_intent_keyword() {
13145        let src = "garbage intent I {}";
13146        let pr = recover(src);
13147        assert_eq!(pr.program.declarations.len(), 1);
13148    }
13149
13150    #[test]
13151    fn sync_to_persona_keyword() {
13152        let src = "garbage persona P { name: \"P\" role: \"R\" }";
13153        let pr = recover(src);
13154        assert!(
13155            pr.program
13156                .declarations
13157                .iter()
13158                .any(|d| matches!(d, Declaration::Persona(_))),
13159            "persona not recovered: decls = {:?}",
13160            pr.program.declarations.len()
13161        );
13162    }
13163
13164    #[test]
13165    fn sync_to_run_keyword() {
13166        let src = "garbage run R { input: { user_message: \"hi\" } }";
13167        let pr = recover(src);
13168        // Either Run was parsed, or recovery still produced ≥1 err.
13169        assert!(pr.has_errors());
13170    }
13171
13172    // ── parse_result_api ─────────────────────────────────────────
13173
13174    #[test]
13175    fn parse_result_has_errors_and_is_clean_invert() {
13176        let pr_clean = recover("flow F() { }");
13177        assert!(pr_clean.is_clean());
13178        assert!(!pr_clean.has_errors());
13179
13180        let pr_err = recover("garbage");
13181        assert!(!pr_err.is_clean());
13182        assert!(pr_err.has_errors());
13183    }
13184
13185    #[test]
13186    fn parse_result_program_field_holds_partial_program() {
13187        let pr = recover("garbage flow F() { }");
13188        assert!(!pr.program.declarations.is_empty());
13189    }
13190
13191    #[test]
13192    fn parse_result_errors_carry_line_and_column() {
13193        let pr = recover("garbage");
13194        assert!(!pr.errors.is_empty());
13195        let e = &pr.errors[0];
13196        assert!(e.line >= 1);
13197        // Column may be 0-based or 1-based depending on lexer;
13198        // accept anything ≥ 0.
13199        let _ = e.column;
13200        assert!(!e.message.is_empty());
13201    }
13202
13203    #[test]
13204    fn parse_result_debug_renders() {
13205        let pr = recover("flow F() { }");
13206        let s = format!("{pr:?}");
13207        assert!(s.contains("ParseResult"));
13208    }
13209
13210    // ── edge_cases ───────────────────────────────────────────────
13211
13212    #[test]
13213    fn empty_source_is_clean() {
13214        let pr = recover("");
13215        assert!(pr.is_clean());
13216        assert!(pr.program.declarations.is_empty());
13217    }
13218
13219    #[test]
13220    fn whitespace_only_source_is_clean() {
13221        let pr = recover("   \n\n\t  \n");
13222        assert!(pr.is_clean());
13223        assert!(pr.program.declarations.is_empty());
13224    }
13225
13226    #[test]
13227    fn only_garbage_does_not_crash() {
13228        // Lex-clean garbage tokens (avoids AxonLexerError).
13229        let pr = recover("foo bar baz { qux quux } corge { grault }");
13230        assert!(pr.has_errors());
13231    }
13232
13233    #[test]
13234    fn unbalanced_close_brace_does_not_crash() {
13235        let pr = recover("} flow F() { }");
13236        // Recovery must keep walking past stray `}`.
13237        let names: Vec<&str> = pr
13238            .program
13239            .declarations
13240            .iter()
13241            .filter_map(|d| match d {
13242                Declaration::Flow(f) => Some(f.name.as_str()),
13243                _ => None,
13244            })
13245            .collect();
13246        assert!(names.contains(&"F"), "F not recovered: {names:?}");
13247    }
13248
13249    #[test]
13250    fn error_at_eof_does_not_loop() {
13251        // Truncated declaration. Must terminate; finite errors.
13252        let pr = recover("flow F() { ");
13253        // Either errored or somehow accepted — but must terminate.
13254        let _ = pr.errors.len();
13255    }
13256
13257    #[test]
13258    fn nested_braces_inside_error_still_balance() {
13259        // Walker must respect brace depth so a `}` inside a malformed
13260        // block does not prematurely sync.
13261        let src = "flow F() { not_a_step { inner } } flow G() { }";
13262        let pr = recover(src);
13263        let names: Vec<&str> = pr
13264            .program
13265            .declarations
13266            .iter()
13267            .filter_map(|d| match d {
13268                Declaration::Flow(f) => Some(f.name.as_str()),
13269                _ => None,
13270            })
13271            .collect();
13272        assert!(names.contains(&"G"), "G not recovered: {names:?}");
13273    }
13274
13275    // ── robustness_fuzz ──────────────────────────────────────────
13276    //
13277    // Deterministic-seeded mutator (xorshift). 100 buckets ×
13278    // 10 mutations = 1000 iterations, byte-bounded so fuzz time
13279    // stays under 1 s on a release build. Recovery must NEVER crash;
13280    // lexer-level errors are out of scope (lexer recovery is its own
13281    // step). 28.b mirrors this with the same structure.
13282
13283    #[derive(Clone, Copy)]
13284    struct Xorshift(u64);
13285    impl Xorshift {
13286        fn next(&mut self) -> u64 {
13287            let mut x = self.0;
13288            x ^= x << 13;
13289            x ^= x >> 7;
13290            x ^= x << 17;
13291            self.0 = x;
13292            x
13293        }
13294        fn pick<T: Copy>(&mut self, slice: &[T]) -> T {
13295            slice[(self.next() as usize) % slice.len()]
13296        }
13297    }
13298
13299    fn mutate(src: &str, rng: &mut Xorshift) -> String {
13300        let mut bytes: Vec<u8> = src.bytes().collect();
13301        if bytes.is_empty() {
13302            return src.to_string();
13303        }
13304        let op = rng.next() % 4;
13305        let pos = (rng.next() as usize) % bytes.len();
13306        // Stick to ASCII-safe printable bytes to keep input lex-able
13307        // most of the time. AxonLexerError is still possible and is
13308        // tolerated by the recovery contract.
13309        let safe: &[u8] = b"abcdefghijklmnopqrstuvwxyz {}();:,_0123456789";
13310        match op {
13311            0 => {
13312                bytes.remove(pos);
13313            }
13314            1 => {
13315                let b = rng.pick(safe);
13316                bytes.insert(pos, b);
13317            }
13318            2 if pos + 1 < bytes.len() => {
13319                bytes.swap(pos, pos + 1);
13320            }
13321            _ => {
13322                let b = rng.pick(safe);
13323                bytes[pos] = b;
13324            }
13325        }
13326        // Lossy decode: mutator may have produced invalid UTF-8;
13327        // strip non-ASCII before handing to the lexer.
13328        bytes.retain(|b| b.is_ascii());
13329        String::from_utf8_lossy(&bytes).into_owned()
13330    }
13331
13332    #[test]
13333    fn fuzz_recovery_never_crashes() {
13334        let seed_bases = [
13335            "flow F() { }",
13336            "intent I { }",
13337            "persona P { name: \"P\" role: \"R\" }",
13338            "intent J { ask: \"a\" }",
13339            "type T = String",
13340        ];
13341        // 100 buckets × 10 mutations = 1000 iterations, deterministic.
13342        for (bucket, base) in (0..100u64).zip(seed_bases.iter().cycle()) {
13343            let mut rng = Xorshift(0x1234_5678_9abc_def0_u64.wrapping_add(bucket));
13344            let mut current = (*base).to_string();
13345            for _ in 0..10 {
13346                current = mutate(&current, &mut rng);
13347                // Lexer may reject; that's outside parser-recovery
13348                // scope (28.b/c). Skip those iterations.
13349                let toks = match Lexer::new(&current, "<fuzz>").tokenize() {
13350                    Ok(t) => t,
13351                    Err(_) => continue,
13352                };
13353                // Recovery must not panic on any well-lexed input.
13354                let _pr = Parser::new(toks).parse_with_recovery();
13355            }
13356        }
13357    }
13358
13359    // ── integration_with_v1_19_4_colon_diagnostic ────────────────
13360
13361    #[test]
13362    fn missing_colon_hint_preserved_under_recovery() {
13363        // The Rust frontend's strict `parse()` carries the same
13364        // colon diagnostic shape as the Python side. Recovery mode
13365        // must not erase it.
13366        let src = "flow F() { run R { input { user_message: \"hi\" } } }";
13367        let pr = recover(src);
13368        // Either the parser accepts this (some shape may be valid)
13369        // or it errors — but if it errors, the message must surface
13370        // the diagnostic content.
13371        if !pr.errors.is_empty() {
13372            let any_msg = pr.errors.iter().any(|e| !e.message.is_empty());
13373            assert!(any_msg);
13374        }
13375    }
13376
13377    // ── recovery preserves declaration ordering ──────────────────
13378
13379    #[test]
13380    fn recovered_declarations_appear_in_source_order() {
13381        let src = "flow A() { } garbage flow B() { } garbage flow C() { }";
13382        let pr = recover(src);
13383        let names: Vec<&str> = pr
13384            .program
13385            .declarations
13386            .iter()
13387            .filter_map(|d| match d {
13388                Declaration::Flow(f) => Some(f.name.as_str()),
13389                _ => None,
13390            })
13391            .collect();
13392        assert_eq!(names, vec!["A", "B", "C"]);
13393    }
13394}
13395
13396// ── v1.20.0 — Source-context diagnostic block test pack ───────────────────
13397//
13398// Mirror of `tests/test_fase28_source_context.py` (Python side, 28.d).
13399// The render output must be byte-identical to the Python `SourceSnippet.render`
13400// on the same input — D7 ratified (cross-stack drift gate). Golden strings
13401// in `golden_*` tests are duplicated verbatim in the Python pack; edits
13402// here MUST be mirrored on the Python side and vice versa.
13403#[cfg(test)]
13404mod source_context_tests {
13405    use super::*;
13406    use crate::lexer::Lexer;
13407
13408    fn snippet(source: &str, line: u32, column: u32, filename: &str) -> String {
13409        SourceSnippet::new(
13410            source.to_string(),
13411            line,
13412            column,
13413            filename.to_string(),
13414        )
13415        .render()
13416    }
13417
13418    // ── Pure rendering ──────────────────────────────────────────
13419
13420    #[test]
13421    fn rustc_style_block_for_middle_line() {
13422        let src = "line one\nline two\nline three\nline four\nline five";
13423        let out = snippet(src, 3, 6, "x.axon");
13424        assert!(out.contains("--> x.axon:3:6"));
13425        assert!(out.contains("1 | line one"));
13426        assert!(out.contains("2 | line two"));
13427        assert!(out.contains("3 | line three"));
13428        assert!(out.contains("4 | line four"));
13429        assert!(out.contains("5 | line five"));
13430        // Caret col 6 → 5-space pad. Empty gutter is 1 space (gutter=1).
13431        assert!(out.contains("\n  |      ^"), "out:\n{out}");
13432    }
13433
13434    #[test]
13435    fn caret_column_one_renders_correctly() {
13436        let out = snippet("abc\n", 1, 1, "<source>");
13437        assert!(out.contains("\n  | ^"));
13438    }
13439
13440    #[test]
13441    fn first_line_clamps_context_before_to_zero() {
13442        let src = "first\nsecond\nthird\nfourth\nfifth";
13443        let out = snippet(src, 1, 1, "<source>");
13444        assert!(out.contains("1 | first"));
13445        assert!(out.contains("2 | second"));
13446        assert!(out.contains("3 | third"));
13447        assert!(!out.contains("4 | fourth"));
13448    }
13449
13450    #[test]
13451    fn last_line_clamps_context_after_to_eof() {
13452        let src = "first\nsecond\nthird\nfourth\nfifth";
13453        let out = snippet(src, 5, 2, "<source>");
13454        assert!(out.contains("5 | fifth"));
13455        assert!(out.contains("3 | third"));
13456        assert!(out.contains("4 | fourth"));
13457        assert!(!out.contains("2 | second"));
13458    }
13459
13460    #[test]
13461    fn gutter_width_grows_with_line_count() {
13462        let src: String = (1..=12).map(|i| format!("line{i}")).collect::<Vec<_>>().join("\n");
13463        let out = snippet(&src, 12, 1, "<source>");
13464        assert!(out.contains("12 | line12"));
13465        assert!(out.contains("10 | line10"));
13466    }
13467
13468    // ── Edge cases ──────────────────────────────────────────────
13469
13470    #[test]
13471    fn empty_source_returns_empty() {
13472        assert_eq!(snippet("", 1, 1, "<source>"), "");
13473    }
13474
13475    #[test]
13476    fn zero_line_returns_empty() {
13477        assert_eq!(snippet("hi", 0, 1, "<source>"), "");
13478    }
13479
13480    #[test]
13481    fn out_of_range_line_returns_empty() {
13482        assert_eq!(snippet("hi", 99, 1, "<source>"), "");
13483    }
13484
13485    #[test]
13486    fn caret_clamps_past_eol() {
13487        let out = snippet("hello", 1, 50, "<source>");
13488        assert!(out.contains("\n  |      ^"), "out:\n{out}");
13489    }
13490
13491    #[test]
13492    fn unicode_codepoint_count_for_caret_clamp() {
13493        // "héllo" = 5 codepoints; column past EOL clamps to 6.
13494        let out = snippet("héllo", 1, 99, "<source>");
13495        assert!(out.contains("\n  |      ^"), "out:\n{out}");
13496    }
13497
13498    #[test]
13499    fn trailing_newline_does_not_create_phantom_last_line() {
13500        let out = snippet("first\nsecond\n", 2, 1, "<source>");
13501        assert!(!out.contains("3 |"));
13502        assert!(out.contains("2 | second"));
13503    }
13504
13505    // ── Parser attach plumbing ──────────────────────────────────
13506
13507    fn lex(src: &str) -> Vec<Token> {
13508        Lexer::new(src, "<test>").tokenize().expect("lex")
13509    }
13510
13511    #[test]
13512    fn strict_parse_attaches_snippet_when_source_given() {
13513        let src = "garbage_token\nflow F() { }";
13514        let err = Parser::new(lex(src))
13515            .with_source(src, "x.axon")
13516            .parse()
13517            .expect_err("must error");
13518        assert!(err.source_snippet.is_some());
13519        let display = format!("{err}");
13520        assert!(display.contains("--> x.axon:"), "display: {display}");
13521    }
13522
13523    #[test]
13524    fn strict_parse_no_snippet_when_no_source() {
13525        let src = "garbage_token";
13526        let err = Parser::new(lex(src)).parse().expect_err("must error");
13527        assert!(err.source_snippet.is_none());
13528        let display = format!("{err}");
13529        assert!(!display.contains("\n  -->"));
13530    }
13531
13532    #[test]
13533    fn every_recovered_error_has_snippet() {
13534        let src = "garbage1\nflow F() { }\ngarbage2\nflow G() { }";
13535        let result = Parser::new(lex(src))
13536            .with_source(src, "multi.axon")
13537            .parse_with_recovery();
13538        assert!(!result.errors.is_empty());
13539        for err in &result.errors {
13540            assert!(err.source_snippet.is_some());
13541            let display = format!("{err}");
13542            assert!(
13543                display.contains("--> multi.axon:"),
13544                "display: {display}"
13545            );
13546        }
13547    }
13548
13549    #[test]
13550    fn recovery_no_snippet_when_no_source() {
13551        let src = "garbage1 garbage2";
13552        let result = Parser::new(lex(src)).parse_with_recovery();
13553        for err in &result.errors {
13554            assert!(err.source_snippet.is_none());
13555        }
13556    }
13557
13558    #[test]
13559    fn snippet_points_at_correct_line_for_each_error() {
13560        let src = "garbage_a\nflow F() { }\ngarbage_b\nflow G() { }";
13561        let result = Parser::new(lex(src))
13562            .with_source(src, "x")
13563            .parse_with_recovery();
13564        for err in &result.errors {
13565            let sn = err.source_snippet.as_ref().expect("snippet");
13566            assert_eq!(sn.line, err.line);
13567        }
13568    }
13569
13570    // ── Backwards-compat ────────────────────────────────────────
13571
13572    #[test]
13573    fn legacy_constructor_still_works() {
13574        let src = "flow F() { }";
13575        let prog = Parser::new(lex(src)).parse().expect("clean");
13576        assert_eq!(prog.declarations.len(), 1);
13577    }
13578
13579    #[test]
13580    fn attach_source_idempotent() {
13581        let err = ParseError {
13582            message: "bad".to_string(),
13583            line: 2,
13584            column: 3,
13585            ..Default::default()
13586        };
13587        let err2 = err.clone().attach_source("a\nb\nc\n", "f.axon");
13588        let first = format!("{err2}");
13589        let err3 = err.attach_source("a\nb\nc\n", "f.axon");
13590        let second = format!("{err3}");
13591        assert_eq!(first, second);
13592    }
13593
13594    #[test]
13595    fn attach_source_noop_when_line_zero() {
13596        let err = ParseError {
13597            message: "bad".to_string(),
13598            line: 0,
13599            column: 0,
13600            ..Default::default()
13601        };
13602        let err = err.attach_source("a\nb\nc\n", "f.axon");
13603        assert!(err.source_snippet.is_none());
13604    }
13605
13606    // ── Cross-stack golden parity ───────────────────────────────
13607    // These golden strings are duplicated verbatim in the Python
13608    // test pack at `tests/test_fase28_source_context.py::TestRustParityShape`.
13609    // Edits here MUST be mirrored in the Python pack — D7.
13610
13611    #[test]
13612    fn golden_simple_three_line_block() {
13613        let src = "alpha\nbeta\ngamma";
13614        let out = snippet(src, 2, 3, "g.axon");
13615        // Note: gutter=1, so empty_gutter=" " (one space). The
13616        // " --> ..." line therefore starts with two spaces ("<empty>"
13617        // + literal " --> ...").
13618        let expected = concat!(
13619            "  --> g.axon:2:3\n",
13620            "  |\n",
13621            "1 | alpha\n",
13622            "2 | beta\n",
13623            "  |   ^\n",
13624            "3 | gamma",
13625        );
13626        assert_eq!(out, expected);
13627    }
13628
13629    #[test]
13630    fn golden_first_line_caret() {
13631        let src = "abc\ndef\n";
13632        let out = snippet(src, 1, 1, "x");
13633        let expected = concat!(
13634            "  --> x:1:1\n",
13635            "  |\n",
13636            "1 | abc\n",
13637            "  | ^\n",
13638            "2 | def",
13639        );
13640        assert_eq!(out, expected);
13641    }
13642
13643    #[test]
13644    fn golden_two_digit_gutter() {
13645        let src: String = (1..=11)
13646            .map(|i| format!("L{i}"))
13647            .collect::<Vec<_>>()
13648            .join("\n");
13649        let out = snippet(&src, 10, 2, "big");
13650        let expected = concat!(
13651            "   --> big:10:2\n",
13652            "   |\n",
13653            " 8 | L8\n",
13654            " 9 | L9\n",
13655            "10 | L10\n",
13656            "   |  ^\n",
13657            "11 | L11",
13658        );
13659        assert_eq!(out, expected);
13660    }
13661}
13662
13663// ── v1.20.0 — Parser integration tests for smart-suggest ──────────────────
13664//
13665// Mirror of `tests/test_fase28_smart_suggest.py::TestParserIntegration`.
13666// Verifies that the parser actually wires `suggest_for` into the
13667// unknown-keyword diagnostic at both error sites — top-level and
13668// flow-body.
13669#[cfg(test)]
13670mod smart_suggest_parser_tests {
13671    use super::*;
13672    use crate::lexer::Lexer;
13673
13674    fn lex(src: &str) -> Vec<Token> {
13675        Lexer::new(src, "<test>").tokenize().expect("lex")
13676    }
13677
13678    #[test]
13679    fn top_level_typo_suggests_flow() {
13680        let src = "flwo F() { }";
13681        let err = Parser::new(lex(src)).parse().expect_err("must error");
13682        assert!(
13683            err.message.contains("Did you mean `flow`?"),
13684            "msg: {}",
13685            err.message
13686        );
13687    }
13688
13689    #[test]
13690    fn top_level_unknown_far_no_suggestion() {
13691        let src = "qwerty F() { }";
13692        let err = Parser::new(lex(src)).parse().expect_err("must error");
13693        assert!(
13694            !err.message.contains("Did you mean"),
13695            "msg: {}",
13696            err.message
13697        );
13698    }
13699
13700    #[test]
13701    fn flow_body_typo_suggests_step() {
13702        let src = "flow F() { stepp S {} }";
13703        let err = Parser::new(lex(src)).parse().expect_err("must error");
13704        assert!(
13705            err.message.contains("Did you mean `step`"),
13706            "msg: {}",
13707            err.message
13708        );
13709    }
13710
13711    #[test]
13712    fn flow_body_typo_suggests_reason() {
13713        let src = "flow F() { reasn R {} }";
13714        let err = Parser::new(lex(src)).parse().expect_err("must error");
13715        assert!(
13716            err.message.contains("Did you mean `reason`?"),
13717            "msg: {}",
13718            err.message
13719        );
13720    }
13721
13722    #[test]
13723    fn recovery_mode_carries_hint() {
13724        let src = "flwo F() { }";
13725        let result = Parser::new(lex(src)).parse_with_recovery();
13726        assert!(
13727            result
13728                .errors
13729                .iter()
13730                .any(|e| e.message.contains("Did you mean `flow`?")),
13731            "errors: {:?}",
13732            result.errors
13733        );
13734    }
13735}
13736
13737// ── v1.30.0 — mutate / purge where-clause capture ────────────────
13738
13739#[cfg(test)]
13740mod mutate_purge_where_tests {
13741    use super::*;
13742
13743    fn parse(src: &str) -> Program {
13744        let tokens = crate::lexer::Lexer::new(src, "<test>")
13745            .tokenize()
13746            .expect("lex");
13747        Parser::new(tokens).parse().expect("parse")
13748    }
13749
13750    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
13751        for d in &prog.declarations {
13752            if let Declaration::Flow(f) = d {
13753                if f.name == flow {
13754                    return f.body.first().expect("flow has at least one step");
13755                }
13756            }
13757        }
13758        panic!("flow `{flow}` not found");
13759    }
13760
13761    #[test]
13762    fn mutate_captures_its_where_clause() {
13763        // Pre-35.m the `{ where: }` block was skipped — every mutate
13764        // ran whole-store. It must now reach `where_expr`.
13765        let prog =
13766            parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
13767        match first_step(&prog, "F") {
13768            FlowStep::Mutate(m) => {
13769                assert_eq!(m.store_name, "accounts");
13770                assert_eq!(m.where_expr, "id = 1");
13771            }
13772            other => panic!("expected Mutate, got {other:?}"),
13773        }
13774    }
13775
13776    #[test]
13777    fn purge_captures_its_where_clause() {
13778        let prog =
13779            parse("flow F() -> Unit { purge logs { where: \"ts < 100\" } }");
13780        match first_step(&prog, "F") {
13781            FlowStep::Purge(p) => {
13782                assert_eq!(p.store_name, "logs");
13783                assert_eq!(p.where_expr, "ts < 100");
13784            }
13785            other => panic!("expected Purge, got {other:?}"),
13786        }
13787    }
13788
13789    #[test]
13790    fn mutate_without_a_where_block_is_a_whole_store_op() {
13791        // No `{ where: }` → an empty filter → the runtime renders
13792        // `WHERE TRUE` (every row). A valid, intentional op.
13793        let prog = parse("flow F() -> Unit { mutate accounts }");
13794        match first_step(&prog, "F") {
13795            FlowStep::Mutate(m) => {
13796                assert_eq!(m.store_name, "accounts");
13797                assert_eq!(m.where_expr, "");
13798            }
13799            other => panic!("expected Mutate, got {other:?}"),
13800        }
13801    }
13802}
13803
13804// ── v1.30.0 — persist field-block capture ────────────────────────
13805
13806#[cfg(test)]
13807mod persist_fields_tests {
13808    use super::*;
13809
13810    fn parse(src: &str) -> Program {
13811        let tokens = crate::lexer::Lexer::new(src, "<test>")
13812            .tokenize()
13813            .expect("lex");
13814        Parser::new(tokens).parse().expect("parse")
13815    }
13816
13817    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
13818        for d in &prog.declarations {
13819            if let Declaration::Flow(f) = d {
13820                if f.name == flow {
13821                    return f.body.first().expect("flow has at least one step");
13822                }
13823            }
13824        }
13825        panic!("flow `{flow}` not found");
13826    }
13827
13828    #[test]
13829    fn persist_captures_its_field_block() {
13830        // Pre-35.o the `{ col: value }` block was skipped — every
13831        // persist wrote the whole binding context. It must now reach
13832        // `fields`, in source order, with value expressions raw.
13833        let prog = parse(
13834            "flow F() -> Unit { persist into chat_history { \
13835             session_id: \"${session_id}\" sender: \"user\" \
13836             content: \"${message}\" } }",
13837        );
13838        match first_step(&prog, "F") {
13839            FlowStep::Persist(p) => {
13840                assert_eq!(p.store_name, "chat_history");
13841                assert_eq!(
13842                    p.fields,
13843                    vec![
13844                        ("session_id".to_string(), "${session_id}".to_string()),
13845                        ("sender".to_string(), "user".to_string()),
13846                        ("content".to_string(), "${message}".to_string()),
13847                    ]
13848                );
13849            }
13850            other => panic!("expected Persist, got {other:?}"),
13851        }
13852    }
13853
13854    #[test]
13855    fn persist_without_a_block_keeps_the_user_bindings_fallback() {
13856        // No `{ }` → empty `fields` → the runtime falls back to the
13857        // v1.30.0 user-bindings row. Backward-compatible.
13858        let prog = parse("flow F() -> Unit { persist events }");
13859        match first_step(&prog, "F") {
13860            FlowStep::Persist(p) => {
13861                assert_eq!(p.store_name, "events");
13862                assert!(p.fields.is_empty());
13863            }
13864            other => panic!("expected Persist, got {other:?}"),
13865        }
13866    }
13867
13868    #[test]
13869    fn persist_accepts_the_optional_into_connector() {
13870        // `persist into X` and `persist X` resolve to the SAME store
13871        // name — pre-35.o `into` was captured AS the store name.
13872        let with =
13873            parse("flow F() -> Unit { persist into accounts { id: \"1\" } }");
13874        let without =
13875            parse("flow F() -> Unit { persist accounts { id: \"1\" } }");
13876        for prog in [&with, &without] {
13877            match first_step(prog, "F") {
13878                FlowStep::Persist(p) => assert_eq!(p.store_name, "accounts"),
13879                other => panic!("expected Persist, got {other:?}"),
13880            }
13881        }
13882    }
13883
13884    #[test]
13885    fn persist_into_without_a_block_resolves_the_store_name() {
13886        // `persist into events` — the `into` connector is skipped, the
13887        // store name is `events` (not `into`). Lateral bug closed.
13888        let prog = parse("flow F() -> Unit { persist into events }");
13889        match first_step(&prog, "F") {
13890            FlowStep::Persist(p) => {
13891                assert_eq!(p.store_name, "events");
13892                assert!(p.fields.is_empty());
13893            }
13894            other => panic!("expected Persist, got {other:?}"),
13895        }
13896    }
13897
13898    #[test]
13899    fn persist_fields_lower_into_the_ir() {
13900        // The IR generator must carry `fields` onto `IRPersistStep`
13901        // so the runtime reads exactly the declared columns.
13902        let prog = parse(
13903            "flow F() -> Unit { persist into chat { content: \"${msg}\" } }",
13904        );
13905        let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
13906        let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
13907        match flow.steps.first().expect("one step") {
13908            crate::ir_nodes::IRFlowNode::Persist(p) => {
13909                assert_eq!(p.store_name, "chat");
13910                assert_eq!(
13911                    p.fields,
13912                    vec![("content".to_string(), "${msg}".to_string())]
13913                );
13914            }
13915            other => panic!("expected IRFlowNode::Persist, got {other:?}"),
13916        }
13917    }
13918}
13919
13920// ── v1.30.0 — mutate SET-field-block capture ─────────────────────
13921
13922#[cfg(test)]
13923mod mutate_fields_tests {
13924    use super::*;
13925
13926    fn parse(src: &str) -> Program {
13927        let tokens = crate::lexer::Lexer::new(src, "<test>")
13928            .tokenize()
13929            .expect("lex");
13930        Parser::new(tokens).parse().expect("parse")
13931    }
13932
13933    fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
13934        for d in &prog.declarations {
13935            if let Declaration::Flow(f) = d {
13936                if f.name == flow {
13937                    return f.body.first().expect("flow has at least one step");
13938                }
13939            }
13940        }
13941        panic!("flow `{flow}` not found");
13942    }
13943
13944    #[test]
13945    fn mutate_captures_its_set_field_block() {
13946        // Pre-35.p every key but `where:` was skipped — the runtime
13947        // SET every flow binding. The SET columns must now reach
13948        // `fields`, in source order, with `where:` still captured.
13949        let prog = parse(
13950            "flow F() -> Unit { mutate accounts { where: \"id = ${id}\" \
13951             balance: \"${new_balance}\" status: \"active\" } }",
13952        );
13953        match first_step(&prog, "F") {
13954            FlowStep::Mutate(m) => {
13955                assert_eq!(m.store_name, "accounts");
13956                assert_eq!(m.where_expr, "id = ${id}");
13957                assert_eq!(
13958                    m.fields,
13959                    vec![
13960                        ("balance".to_string(), "${new_balance}".to_string()),
13961                        ("status".to_string(), "active".to_string()),
13962                    ]
13963                );
13964            }
13965            other => panic!("expected Mutate, got {other:?}"),
13966        }
13967    }
13968
13969    #[test]
13970    fn mutate_where_only_block_has_no_set_fields() {
13971        // A `{ where: }`-only block → empty `fields` → the runtime
13972        // falls back to the v1.31.0 user-bindings SET.
13973        let prog =
13974            parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
13975        match first_step(&prog, "F") {
13976            FlowStep::Mutate(m) => {
13977                assert_eq!(m.where_expr, "id = 1");
13978                assert!(m.fields.is_empty());
13979            }
13980            other => panic!("expected Mutate, got {other:?}"),
13981        }
13982    }
13983
13984    #[test]
13985    fn mutate_with_no_block_is_a_whole_store_op() {
13986        // No block at all → empty where + empty fields (a whole-store
13987        // UPDATE from user bindings) — unchanged from 35.m.
13988        let prog = parse("flow F() -> Unit { mutate accounts }");
13989        match first_step(&prog, "F") {
13990            FlowStep::Mutate(m) => {
13991                assert_eq!(m.store_name, "accounts");
13992                assert_eq!(m.where_expr, "");
13993                assert!(m.fields.is_empty());
13994            }
13995            other => panic!("expected Mutate, got {other:?}"),
13996        }
13997    }
13998
13999    #[test]
14000    fn mutate_fields_lower_into_the_ir() {
14001        let prog = parse(
14002            "flow F() -> Unit { mutate t { where: \"id = 1\" v: \"${x}\" } }",
14003        );
14004        let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
14005        let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
14006        match flow.steps.first().expect("one step") {
14007            crate::ir_nodes::IRFlowNode::Mutate(m) => {
14008                assert_eq!(m.where_expr, "id = 1");
14009                assert_eq!(
14010                    m.fields,
14011                    vec![("v".to_string(), "${x}".to_string())]
14012                );
14013            }
14014            other => panic!("expected IRFlowNode::Mutate, got {other:?}"),
14015        }
14016    }
14017}
14018